diff options
Diffstat (limited to 'core/java')
278 files changed, 18216 insertions, 6741 deletions
diff --git a/core/java/android/accounts/AccountAuthenticatorActivity.java b/core/java/android/accounts/AccountAuthenticatorActivity.java index 5cce6da..6a55ddf 100644 --- a/core/java/android/accounts/AccountAuthenticatorActivity.java +++ b/core/java/android/accounts/AccountAuthenticatorActivity.java @@ -26,7 +26,7 @@ import android.os.Bundle; * to handle the request then it can have the activity extend AccountAuthenticatorActivity. * The AbstractAccountAuthenticator passes in the response to the intent using the following: * <pre> - * intent.putExtra(Constants.ACCOUNT_AUTHENTICATOR_RESPONSE_KEY, response); + * intent.putExtra({@link AccountManager#KEY_ACCOUNT_AUTHENTICATOR_RESPONSE}, response); * </pre> * The activity then sets the result that is to be handed to the response via * {@link #setAccountAuthenticatorResult(android.os.Bundle)}. diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java index 5bdc79d..2156425 100644 --- a/core/java/android/accounts/AccountManager.java +++ b/core/java/android/accounts/AccountManager.java @@ -179,6 +179,7 @@ public class AccountManager { public static final String KEY_PASSWORD = "password"; public static final String KEY_ACCOUNTS = "accounts"; + public static final String KEY_ACCOUNT_AUTHENTICATOR_RESPONSE = "accountAuthenticatorResponse"; public static final String KEY_ACCOUNT_MANAGER_RESPONSE = "accountManagerResponse"; public static final String KEY_AUTHENTICATOR_TYPES = "authenticator_types"; @@ -1269,7 +1270,7 @@ public class AccountManager { /** Handles the responses from the AccountManager */ private class Response extends IAccountManagerResponse.Stub { public void onResult(Bundle bundle) { - Intent intent = bundle.getParcelable("intent"); + Intent intent = bundle.getParcelable(KEY_INTENT); if (intent != null && mActivity != null) { // since the user provided an Activity we will silently start intents // that we see diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java index 93983a6..20d5b96 100644 --- a/core/java/android/accounts/AccountManagerService.java +++ b/core/java/android/accounts/AccountManagerService.java @@ -37,6 +37,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.RegisteredServicesCache; import android.content.pm.RegisteredServicesCacheListener; +import android.content.res.Resources; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; @@ -73,8 +74,7 @@ import java.util.concurrent.atomic.AtomicReference; * accounts on the device. Some of these calls are implemented with the help of the corresponding * {@link IAccountAuthenticator} services. This service is not accessed by users directly, * instead one uses an instance of {@link AccountManager}, which can be accessed as follows: - * AccountManager accountManager = - * (AccountManager)context.getSystemService(Context.ACCOUNT_SERVICE) + * AccountManager accountManager = AccountManager.get(context); * @hide */ public class AccountManagerService @@ -1064,14 +1064,18 @@ public class AccountManagerService } catch (PackageManager.NameNotFoundException e) { throw new IllegalArgumentException("unknown account type: " + accountType); } - return authContext.getString(serviceInfo.type.labelId); + try { + return authContext.getString(serviceInfo.type.labelId); + } catch (Resources.NotFoundException e) { + throw new IllegalArgumentException("unknown account type: " + accountType); + } } private Intent newGrantCredentialsPermissionIntent(Account account, int uid, AccountAuthenticatorResponse response, String authTokenType, String authTokenLabel) { Intent intent = new Intent(mContext, GrantCredentialsPermissionActivity.class); - // See FLAT_ACTIVITY_NEW_TASK docs for limitations and benefits of the flag. + // See FLAG_ACTIVITY_NEW_TASK docs for limitations and benefits of the flag. // Since it was set in Eclair+ we can't change it without breaking apps using // the intent from a non-Activity context. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/core/java/android/accounts/ChooseAccountActivity.java b/core/java/android/accounts/ChooseAccountActivity.java index 293df78..bfbae24 100644 --- a/core/java/android/accounts/ChooseAccountActivity.java +++ b/core/java/android/accounts/ChooseAccountActivity.java @@ -18,6 +18,7 @@ package android.accounts; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Parcelable; @@ -103,7 +104,12 @@ public class ChooseAccountActivity extends Activity { } catch (PackageManager.NameNotFoundException e) { // Nothing we can do much here, just log if (Log.isLoggable(TAG, Log.WARN)) { - Log.w(TAG, "No icon for account type " + accountType); + Log.w(TAG, "No icon name for account type " + accountType); + } + } catch (Resources.NotFoundException e) { + // Nothing we can do much here, just log + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "No icon resource for account type " + accountType); } } } diff --git a/core/java/android/accounts/GrantCredentialsPermissionActivity.java b/core/java/android/accounts/GrantCredentialsPermissionActivity.java index 89eee6d..0ee683c 100644 --- a/core/java/android/accounts/GrantCredentialsPermissionActivity.java +++ b/core/java/android/accounts/GrantCredentialsPermissionActivity.java @@ -58,6 +58,12 @@ public class GrantCredentialsPermissionActivity extends Activity implements View mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); final Bundle extras = getIntent().getExtras(); + if (extras == null) { + // we were somehow started with bad parameters. abort the activity. + setResult(Activity.RESULT_CANCELED); + finish(); + return; + } // Grant 'account'/'type' to mUID mAccount = extras.getParcelable(EXTRAS_ACCOUNT); @@ -73,8 +79,15 @@ public class GrantCredentialsPermissionActivity extends Activity implements View return; } - final String accountTypeLabel = accountManagerService.getAccountLabel(mAccount.type); - + String accountTypeLabel; + try { + accountTypeLabel = accountManagerService.getAccountLabel(mAccount.type); + } catch (IllegalArgumentException e) { + // label or resource was missing. abort the activity. + setResult(Activity.RESULT_CANCELED); + finish(); + return; + } final TextView authTokenTypeView = (TextView) findViewById(R.id.authtoken_type); authTokenTypeView.setVisibility(View.GONE); diff --git a/core/java/android/animation/AnimatorInflater.java b/core/java/android/animation/AnimatorInflater.java index bcab66e..ed4036d 100644 --- a/core/java/android/animation/AnimatorInflater.java +++ b/core/java/android/animation/AnimatorInflater.java @@ -31,11 +31,11 @@ import java.io.IOException; import java.util.ArrayList; /** - * This class is used to instantiate menu XML files into Animator objects. + * This class is used to instantiate animator XML files into Animator objects. * <p> - * For performance reasons, menu inflation relies heavily on pre-processing of + * For performance reasons, inflation relies heavily on pre-processing of * XML files that is done at build time. Therefore, it is not currently possible - * to use MenuInflater with an XmlPullParser over a plain XML file at runtime; + * to use this inflater with an XmlPullParser over a plain XML file at runtime; * it only works with an XmlPullParser returned from a compiled resource (R. * <em>something</em> file.) */ diff --git a/core/java/android/animation/LayoutTransition.java b/core/java/android/animation/LayoutTransition.java index 22dd3c7..adfda8e 100644 --- a/core/java/android/animation/LayoutTransition.java +++ b/core/java/android/animation/LayoutTransition.java @@ -48,6 +48,18 @@ import java.util.List; * with setting up the basic properties of the animations used in these four situations, * or with setting up custom animations for any or all of the four.</p> * + * <p>By default, the DISAPPEARING animation begins immediately, as does the CHANGE_APPEARING + * animation. The other animations begin after a delay that is set to the default duration + * of the animations. This behavior facilitates a sequence of animations in transitions as + * follows: when an item is being added to a layout, the other children of that container will + * move first (thus creating space for the new item), then the appearing animation will run to + * animate the item being added. Conversely, when an item is removed from a container, the + * animation to remove it will run first, then the animations of the other children in the + * layout will run (closing the gap created in the layout when the item was removed). If this + * default choreography behavior is not desired, the {@link #setDuration(int, long)} and + * {@link #setStartDelay(int, long)} of any or all of the animations can be changed as + * appropriate.</p> + * * <p>The animations specified for the transition, both the defaults and any custom animations * set on the transition object, are templates only. That is, these animations exist to hold the * basic animation properties, such as the duration, start delay, and properties being animated. diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index f562851..1dcaa04 100755 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -1173,13 +1173,11 @@ public class ValueAnimator extends Animator { if (oldValues != null) { int numValues = oldValues.length; anim.mValues = new PropertyValuesHolder[numValues]; - for (int i = 0; i < numValues; ++i) { - anim.mValues[i] = oldValues[i].clone(); - } anim.mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues); for (int i = 0; i < numValues; ++i) { - PropertyValuesHolder valuesHolder = mValues[i]; - anim.mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder); + PropertyValuesHolder newValuesHolder = oldValues[i].clone(); + anim.mValues[i] = newValuesHolder; + anim.mValuesMap.put(newValuesHolder.getPropertyName(), newValuesHolder); } } return anim; diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java index fc5fac6..cac06ec 100644 --- a/core/java/android/app/ActionBar.java +++ b/core/java/android/app/ActionBar.java @@ -107,6 +107,18 @@ public abstract class ActionBar { public static final int DISPLAY_SHOW_CUSTOM = 0x10; /** + * Disable the 'home' element. This may be combined with + * {@link #DISPLAY_SHOW_HOME} to create a non-focusable/non-clickable + * 'home' element. Useful for a level of your app's navigation hierarchy + * where clicking 'home' doesn't do anything. + * + * @see #setDisplayOptions(int) + * @see #setDisplayOptions(int, int) + * @see #setDisplayDisableHomeEnabled(boolean) + */ + public static final int DISPLAY_DISABLE_HOME = 0x20; + + /** * Set the action bar into custom navigation mode, supplying a view * for custom navigation. * @@ -160,6 +172,66 @@ public abstract class ActionBar { public abstract void setCustomView(int resId); /** + * Set the icon to display in the 'home' section of the action bar. + * The action bar will use an icon specified by its style or the + * activity icon by default. + * + * Whether the home section shows an icon or logo is controlled + * by the display option {@link #DISPLAY_USE_LOGO}. + * + * @param resId Resource ID of a drawable to show as an icon. + * + * @see #setDisplayUseLogoEnabled(boolean) + * @see #setDisplayShowHomeEnabled(boolean) + */ + public abstract void setIcon(int resId); + + /** + * Set the icon to display in the 'home' section of the action bar. + * The action bar will use an icon specified by its style or the + * activity icon by default. + * + * Whether the home section shows an icon or logo is controlled + * by the display option {@link #DISPLAY_USE_LOGO}. + * + * @param icon Drawable to show as an icon. + * + * @see #setDisplayUseLogoEnabled(boolean) + * @see #setDisplayShowHomeEnabled(boolean) + */ + public abstract void setIcon(Drawable icon); + + /** + * Set the logo to display in the 'home' section of the action bar. + * The action bar will use a logo specified by its style or the + * activity logo by default. + * + * Whether the home section shows an icon or logo is controlled + * by the display option {@link #DISPLAY_USE_LOGO}. + * + * @param resId Resource ID of a drawable to show as a logo. + * + * @see #setDisplayUseLogoEnabled(boolean) + * @see #setDisplayShowHomeEnabled(boolean) + */ + public abstract void setLogo(int resId); + + /** + * Set the logo to display in the 'home' section of the action bar. + * The action bar will use a logo specified by its style or the + * activity logo by default. + * + * Whether the home section shows an icon or logo is controlled + * by the display option {@link #DISPLAY_USE_LOGO}. + * + * @param logo Drawable to show as a logo. + * + * @see #setDisplayUseLogoEnabled(boolean) + * @see #setDisplayShowHomeEnabled(boolean) + */ + public abstract void setLogo(Drawable logo); + + /** * Set the adapter and navigation callback for list navigation mode. * * The supplied adapter will provide views for the expanded list as well as @@ -333,6 +405,21 @@ public abstract class ActionBar { public abstract void setDisplayShowCustomEnabled(boolean showCustom); /** + * Set whether the 'home' affordance on the action bar should be disabled. + * If set, the 'home' element will not be focusable or clickable, useful if + * the user is at the top level of the app's navigation hierarchy. + * + * <p>To set several display options at once, see the setDisplayOptions methods. + * + * @param disableHome true to disable the 'home' element. + * + * @see #setDisplayOptions(int) + * @see #setDisplayOptions(int, int) + * @see #DISPLAY_DISABLE_HOME + */ + public abstract void setDisplayDisableHomeEnabled(boolean disableHome); + + /** * Set the ActionBar's background. * * @param d Background drawable diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index e3fb358..87369ab 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -51,7 +51,6 @@ import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.method.TextKeyListener; import android.util.AttributeSet; -import android.util.Config; import android.util.EventLog; import android.util.Log; import android.util.SparseArray; @@ -2488,6 +2487,7 @@ public class Activity extends ContextThemeWrapper break; case Window.FEATURE_ACTION_BAR: + initActionBar(); mActionBar.dispatchMenuVisibilityChanged(false); break; } @@ -3633,7 +3633,7 @@ public class Activity extends ContextThemeWrapper resultCode = mResultCode; resultData = mResultData; } - if (Config.LOGV) Log.v(TAG, "Finishing self: token=" + mToken); + if (false) Log.v(TAG, "Finishing self: token=" + mToken); try { if (ActivityManagerNative.getDefault() .finishActivity(mToken, resultCode, resultData)) { @@ -4569,7 +4569,7 @@ public class Activity extends ContextThemeWrapper void dispatchActivityResult(String who, int requestCode, int resultCode, Intent data) { - if (Config.LOGV) Log.v( + if (false) Log.v( TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode + ", resCode=" + resultCode + ", data=" + data); mFragments.noteStateNotSaved(); diff --git a/core/java/android/app/ActivityGroup.java b/core/java/android/app/ActivityGroup.java index f1216f9..5b04253 100644 --- a/core/java/android/app/ActivityGroup.java +++ b/core/java/android/app/ActivityGroup.java @@ -110,7 +110,7 @@ public class ActivityGroup extends Activity { if (who != null) { Activity act = mLocalActivityManager.getActivity(who); /* - if (Config.LOGV) Log.v( + if (false) Log.v( TAG, "Dispatching result: who=" + who + ", reqCode=" + requestCode + ", resCode=" + resultCode + ", data=" + data + ", rec=" + rec); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index a70a219..ef2e54a 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -16,6 +16,9 @@ package android.app; +import com.android.internal.app.IUsageStats; +import com.android.internal.os.PkgUsageStats; + import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -26,18 +29,17 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.os.Debug; -import android.os.RemoteException; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; -import com.android.internal.app.IUsageStats; -import com.android.internal.os.PkgUsageStats; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -47,8 +49,7 @@ import java.util.Map; */ public class ActivityManager { private static String TAG = "ActivityManager"; - private static boolean DEBUG = false; - private static boolean localLOGV = DEBUG || android.util.Config.LOGV; + private static boolean localLOGV = false; private final Context mContext; private final Handler mHandler; @@ -283,13 +284,6 @@ public class ActivityManager { public static final int RECENT_IGNORE_UNAVAILABLE = 0x0002; /** - * Flag for use with {@link #getRecentTasks}: also return the thumbnail - * bitmap (if available) for each recent task. - * @hide - */ - public static final int TASKS_GET_THUMBNAILS = 0x0001000; - - /** * Return a list of the tasks that the user has recently launched, with * the most recent being first and older ones after in order. * @@ -319,7 +313,7 @@ public class ActivityManager { /** * Information you can retrieve about a particular task that is currently * "running" in the system. Note that a running task does not mean the - * given task actual has a process it is actively running in; it simply + * given task actually has a process it is actively running in; it simply * means that the user has gone to it and never closed it, but currently * the system may have killed its process and is only holding on to its * last state in order to restart it when the user returns. @@ -474,10 +468,118 @@ public class ActivityManager { return getRunningTasks(maxNum, 0, null); } + /** + * Remove some end of a task's activity stack that is not part of + * the main application. The selected activities will be finished, so + * they are no longer part of the main task. + * + * @param taskId The identifier of the task. + * @param subTaskIndex The number of the sub-task; this corresponds + * to the index of the thumbnail returned by {@link #getTaskThumbnails(int)}. + * @return Returns true if the sub-task was found and was removed. + * + * @hide + */ + public boolean removeSubTask(int taskId, int subTaskIndex) + throws SecurityException { + try { + return ActivityManagerNative.getDefault().removeSubTask(taskId, subTaskIndex); + } catch (RemoteException e) { + // System dead, we will be dead too soon! + return false; + } + } + + /** + * If set, the process of the root activity of the task will be killed + * as part of removing the task. + * @hide + */ + public static final int REMOVE_TASK_KILL_PROCESS = 0x0001; + + /** + * Completely remove the given task. + * + * @param taskId Identifier of the task to be removed. + * @param flags Additional operational flags. May be 0 or + * {@link #REMOVE_TASK_KILL_PROCESS}. + * @return Returns true if the given task was found and removed. + * + * @hide + */ + public boolean removeTask(int taskId, int flags) + throws SecurityException { + try { + return ActivityManagerNative.getDefault().removeTask(taskId, flags); + } catch (RemoteException e) { + // System dead, we will be dead too soon! + return false; + } + } + + /** @hide */ + public static class TaskThumbnails implements Parcelable { + public Bitmap mainThumbnail; + + public int numSubThumbbails; + + /** @hide */ + public IThumbnailRetriever retriever; + + public TaskThumbnails() { + } + + public Bitmap getSubThumbnail(int index) { + try { + return retriever.getThumbnail(index); + } catch (RemoteException e) { + return null; + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + if (mainThumbnail != null) { + dest.writeInt(1); + mainThumbnail.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + dest.writeInt(numSubThumbbails); + dest.writeStrongInterface(retriever); + } + + public void readFromParcel(Parcel source) { + if (source.readInt() != 0) { + mainThumbnail = Bitmap.CREATOR.createFromParcel(source); + } else { + mainThumbnail = null; + } + numSubThumbbails = source.readInt(); + retriever = IThumbnailRetriever.Stub.asInterface(source.readStrongBinder()); + } + + public static final Creator<TaskThumbnails> CREATOR = new Creator<TaskThumbnails>() { + public TaskThumbnails createFromParcel(Parcel source) { + return new TaskThumbnails(source); + } + public TaskThumbnails[] newArray(int size) { + return new TaskThumbnails[size]; + } + }; + + private TaskThumbnails(Parcel source) { + readFromParcel(source); + } + } + /** @hide */ - public Bitmap getTaskThumbnail(int id) throws SecurityException { + public TaskThumbnails getTaskThumbnails(int id) throws SecurityException { try { - return ActivityManagerNative.getDefault().getTaskThumbnail(id); + return ActivityManagerNative.getDefault().getTaskThumbnails(id); } catch (RemoteException e) { // System dead, we will be dead too soon! return null; @@ -693,7 +795,7 @@ public class ActivityManager { public List<RunningServiceInfo> getRunningServices(int maxNum) throws SecurityException { try { - return (List<RunningServiceInfo>)ActivityManagerNative.getDefault() + return ActivityManagerNative.getDefault() .getServices(maxNum, 0); } catch (RemoteException e) { // System dead, we will be dead too soon! @@ -1348,4 +1450,17 @@ public class ActivityManager { return new HashMap<String, Integer>(); } } + + /** + * @param userid the user's id. Zero indicates the default user + * @hide + */ + public boolean switchUser(int userid) { + try { + return ActivityManagerNative.getDefault().switchUser(userid); + } catch (RemoteException e) { + return false; + } + } + } diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index 4beb5cc..f2c9796 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -39,7 +39,6 @@ import android.os.Parcel; import android.os.ServiceManager; import android.os.StrictMode; import android.text.TextUtils; -import android.util.Config; import android.util.Log; import android.util.Singleton; @@ -442,10 +441,10 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } - case GET_TASK_THUMBNAIL_TRANSACTION: { + case GET_TASK_THUMBNAILS_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); int id = data.readInt(); - Bitmap bm = getTaskThumbnail(id); + ActivityManager.TaskThumbnails bm = getTaskThumbnails(id); reply.writeNoException(); if (bm != null) { reply.writeInt(1); @@ -1436,6 +1435,37 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM reply.writeNoException(); return true; } + + case SWITCH_USER_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int userid = data.readInt(); + boolean result = switchUser(userid); + reply.writeNoException(); + reply.writeInt(result ? 1 : 0); + return true; + } + + case REMOVE_SUB_TASK_TRANSACTION: + { + data.enforceInterface(IActivityManager.descriptor); + int taskId = data.readInt(); + int subTaskIndex = data.readInt(); + boolean result = removeSubTask(taskId, subTaskIndex); + reply.writeNoException(); + reply.writeInt(result ? 1 : 0); + return true; + } + + case REMOVE_TASK_TRANSACTION: + { + data.enforceInterface(IActivityManager.descriptor); + int taskId = data.readInt(); + int fl = data.readInt(); + boolean result = removeTask(taskId, fl); + reply.writeNoException(); + reply.writeInt(result ? 1 : 0); + return true; + } } @@ -1449,11 +1479,11 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() { protected IActivityManager create() { IBinder b = ServiceManager.getService("activity"); - if (Config.LOGV) { + if (false) { Log.v("ActivityManager", "default service binder = " + b); } IActivityManager am = asInterface(b); - if (Config.LOGV) { + if (false) { Log.v("ActivityManager", "default service = " + am); } return am; @@ -1870,16 +1900,16 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); return list; } - public Bitmap getTaskThumbnail(int id) throws RemoteException { + public ActivityManager.TaskThumbnails getTaskThumbnails(int id) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeInt(id); - mRemote.transact(GET_TASK_THUMBNAIL_TRANSACTION, data, reply, 0); + mRemote.transact(GET_TASK_THUMBNAILS_TRANSACTION, data, reply, 0); reply.readException(); - Bitmap bm = null; + ActivityManager.TaskThumbnails bm = null; if (reply.readInt() != 0) { - bm = Bitmap.CREATOR.createFromParcel(reply); + bm = ActivityManager.TaskThumbnails.CREATOR.createFromParcel(reply); } data.recycle(); reply.recycle(); @@ -3229,5 +3259,46 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); } + public boolean switchUser(int userid) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(userid); + mRemote.transact(SWITCH_USER_TRANSACTION, data, reply, 0); + reply.readException(); + boolean result = reply.readInt() != 0; + reply.recycle(); + data.recycle(); + return result; + } + + public boolean removeSubTask(int taskId, int subTaskIndex) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(taskId); + data.writeInt(subTaskIndex); + mRemote.transact(REMOVE_SUB_TASK_TRANSACTION, data, reply, 0); + reply.readException(); + boolean result = reply.readInt() != 0; + reply.recycle(); + data.recycle(); + return result; + } + + public boolean removeTask(int taskId, int flags) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(taskId); + data.writeInt(flags); + mRemote.transact(REMOVE_TASK_TRANSACTION, data, reply, 0); + reply.readException(); + boolean result = reply.readInt() != 0; + reply.recycle(); + data.recycle(); + return result; + } + private IBinder mRemote; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 6c8f85f..85e59b3 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -45,6 +45,7 @@ import android.graphics.Canvas; import android.net.IConnectivityManager; import android.net.Proxy; import android.net.ProxyProperties; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Debug; import android.os.Handler; @@ -59,7 +60,6 @@ import android.os.ServiceManager; import android.os.StrictMode; import android.os.SystemClock; import android.util.AndroidRuntimeException; -import android.util.Config; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; @@ -70,7 +70,7 @@ import android.view.HardwareRenderer; import android.view.View; import android.view.ViewDebug; import android.view.ViewManager; -import android.view.ViewRoot; +import android.view.ViewAncestor; import android.view.Window; import android.view.WindowManager; import android.view.WindowManagerImpl; @@ -124,12 +124,12 @@ public final class ActivityThread { public static final String TAG = "ActivityThread"; private static final android.graphics.Bitmap.Config THUMBNAIL_FORMAT = Bitmap.Config.RGB_565; private static final boolean DEBUG = false; - static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; + static final boolean localLOGV = false; static final boolean DEBUG_MESSAGES = false; /** @hide */ public static final boolean DEBUG_BROADCAST = false; private static final boolean DEBUG_RESULTS = false; - private static final boolean DEBUG_BACKUP = false; + private static final boolean DEBUG_BACKUP = true; private static final boolean DEBUG_CONFIGURATION = false; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; private static final Pattern PATTERN_SEMICOLON = Pattern.compile(";"); @@ -338,6 +338,7 @@ public final class ActivityThread { static final class ServiceArgsData { IBinder token; + boolean taskRemoved; int startId; int flags; Intent args; @@ -401,6 +402,8 @@ public final class ActivityThread { String pkg; CompatibilityInfo info; } + + native private void dumpGraphicsInfo(FileDescriptor fd); private final class ApplicationThread extends ApplicationThreadNative { private static final String HEAP_COLUMN = "%17s %8s %8s %8s %8s"; @@ -551,10 +554,11 @@ public final class ActivityThread { queueOrSendMessage(H.UNBIND_SERVICE, s); } - public final void scheduleServiceArgs(IBinder token, int startId, + public final void scheduleServiceArgs(IBinder token, boolean taskRemoved, int startId, int flags ,Intent args) { ServiceArgsData s = new ServiceArgsData(); s.token = token; + s.taskRemoved = taskRemoved; s.startId = startId; s.flags = flags; s.args = args; @@ -716,9 +720,14 @@ public final class ActivityThread { Slog.w(TAG, "dumpActivity failed", e); } } - + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (args != null && args.length == 1 && args[0].equals("graphics")) { + pw.flush(); + dumpGraphicsInfo(fd); + return; + } long nativeMax = Debug.getNativeHeapSize() / 1024; long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024; long nativeFree = Debug.getNativeHeapFreeSize() / 1024; @@ -740,7 +749,7 @@ public final class ActivityThread { long dalvikFree = runtime.freeMemory() / 1024; long dalvikAllocated = dalvikMax - dalvikFree; long viewInstanceCount = ViewDebug.getViewInstanceCount(); - long viewRootInstanceCount = ViewDebug.getViewRootInstanceCount(); + long viewRootInstanceCount = ViewDebug.getViewAncestorInstanceCount(); long appContextInstanceCount = Debug.countInstancesOfClass(ContextImpl.class); long activityInstanceCount = Debug.countInstancesOfClass(Activity.class); int globalAssetCount = AssetManager.getGlobalAssetCount(); @@ -855,7 +864,7 @@ public final class ActivityThread { pw.println(" "); pw.println(" Objects"); - printRow(pw, TWO_COUNT_COLUMNS, "Views:", viewInstanceCount, "ViewRoots:", + printRow(pw, TWO_COUNT_COLUMNS, "Views:", viewInstanceCount, "ViewAncestors:", viewRootInstanceCount); printRow(pw, TWO_COUNT_COLUMNS, "AppContexts:", appContextInstanceCount, @@ -928,7 +937,7 @@ public final class ActivityThread { public static final int HIDE_WINDOW = 106; public static final int RESUME_ACTIVITY = 107; public static final int SEND_RESULT = 108; - public static final int DESTROY_ACTIVITY = 109; + public static final int DESTROY_ACTIVITY = 109; public static final int BIND_APPLICATION = 110; public static final int EXIT_APPLICATION = 111; public static final int NEW_INTENT = 112; @@ -1146,8 +1155,8 @@ public final class ActivityThread { if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + msg.what); } - void maybeSnapshot() { - if (mBoundApplication != null) { + private void maybeSnapshot() { + if (mBoundApplication != null && SamplingProfilerIntegration.isEnabled()) { // convert the *private* ActivityThread.PackageInfo to *public* known // android.content.pm.PackageInfo String packageName = mBoundApplication.info.mPackageName; @@ -1970,24 +1979,26 @@ public final class ActivityThread { BackupAgent agent = null; String classname = data.appInfo.backupAgentName; - if (classname == null) { - if (data.backupMode == IApplicationThread.BACKUP_MODE_INCREMENTAL) { - Slog.e(TAG, "Attempted incremental backup but no defined agent for " - + packageName); - return; + + if (data.backupMode == IApplicationThread.BACKUP_MODE_FULL) { + classname = "android.app.backup.FullBackupAgent"; + if ((data.appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + // system packages can supply their own full-backup agent + if (data.appInfo.fullBackupAgentName != null) { + classname = data.appInfo.fullBackupAgentName; + } } - classname = "android.app.FullBackupAgent"; } + try { IBinder binder = null; try { + if (DEBUG_BACKUP) Slog.v(TAG, "Initializing agent class " + classname); + java.lang.ClassLoader cl = packageInfo.getClassLoader(); - agent = (BackupAgent) cl.loadClass(data.appInfo.backupAgentName).newInstance(); + agent = (BackupAgent) cl.loadClass(classname).newInstance(); // set up the agent's context - if (DEBUG_BACKUP) Slog.v(TAG, "Initializing BackupAgent " - + data.appInfo.backupAgentName); - ContextImpl context = new ContextImpl(); context.init(packageInfo, null, this); context.setOuterContext(agent); @@ -2014,7 +2025,7 @@ public final class ActivityThread { } } catch (Exception e) { throw new RuntimeException("Unable to create BackupAgent " - + data.appInfo.backupAgentName + ": " + e.toString(), e); + + classname + ": " + e.toString(), e); } } @@ -2171,7 +2182,13 @@ public final class ActivityThread { if (data.args != null) { data.args.setExtrasClassLoader(s.getClassLoader()); } - int res = s.onStartCommand(data.args, data.flags, data.startId); + int res; + if (!data.taskRemoved) { + res = s.onStartCommand(data.args, data.flags, data.startId); + } else { + s.onTaskRemoved(data.args); + res = Service.START_TASK_REMOVED_COMPLETE; + } QueuedWork.waitToFinish(); @@ -2701,7 +2718,7 @@ public final class ActivityThread { r.stopped = false; } if (r.activity.mDecor != null) { - if (Config.LOGV) Slog.v( + if (false) Slog.v( TAG, "Handle window " + r + " visibility: " + show); updateVisibility(r, show); } @@ -3442,8 +3459,7 @@ public final class ActivityThread { } final void handleLowMemory() { - ArrayList<ComponentCallbacks> callbacks - = new ArrayList<ComponentCallbacks>(); + ArrayList<ComponentCallbacks> callbacks; synchronized (mPackages) { callbacks = collectComponentCallbacksLocked(true, null); @@ -3474,6 +3490,14 @@ public final class ActivityThread { Process.setArgV0(data.processName); android.ddm.DdmHandleAppName.setAppName(data.processName); + // If the app is Honeycomb MR1 or earlier, switch its AsyncTask + // implementation to use the pool executor. Normally, we use the + // serialized executor as the default. This has to happen in the + // main thread so the main looper is set right. + if (data.appInfo.targetSdkVersion <= 12) { + AsyncTask.setDefaultExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + /* * Before spawning a new process, reset the time zone to be the system time zone. * This needs to be done because the system time zone could have changed after the @@ -3922,7 +3946,7 @@ public final class ActivityThread { info.applicationInfo.sourceDir); return null; } - if (Config.LOGV) Slog.v( + if (false) Slog.v( TAG, "Instantiating local provider " + info.name); // XXX Need to create the correct context for this provider. localProvider.attachInfo(c, info); @@ -3965,7 +3989,7 @@ public final class ActivityThread { sThreadLocal.set(this); mSystemThread = system; if (!system) { - ViewRoot.addFirstDrawHandler(new Runnable() { + ViewAncestor.addFirstDrawHandler(new Runnable() { public void run() { ensureJitEnabled(); } @@ -3995,7 +4019,7 @@ public final class ActivityThread { } } - ViewRoot.addConfigCallback(new ComponentCallbacks() { + ViewAncestor.addConfigCallback(new ComponentCallbacks() { public void onConfigurationChanged(Configuration newConfig) { synchronized (mPackages) { // We need to apply this change to the resources diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 5926929..4cff12f 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -33,7 +33,6 @@ import android.content.pm.IPackageMoveObserver; import android.content.pm.IPackageStatsObserver; import android.content.pm.InstrumentationInfo; import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.PermissionGroupInfo; @@ -41,6 +40,7 @@ import android.content.pm.PermissionInfo; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; @@ -1130,6 +1130,63 @@ final class ApplicationPackageManager extends PackageManager { return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; } + // Multi-user support + + /** + * @hide + */ + @Override + public UserInfo createUser(String name, int flags) { + try { + return mPM.createUser(name, flags); + } catch (RemoteException e) { + // Should never happen! + } + return null; + } + + /** + * @hide + */ + @Override + public List<UserInfo> getUsers() { + // TODO: + // Dummy code, always returns just the primary user + ArrayList<UserInfo> users = new ArrayList<UserInfo>(); + UserInfo primary = new UserInfo(0, "Root!", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY); + users.add(primary); + return users; + } + + /** + * @hide + */ + @Override + public boolean removeUser(int id) { + try { + return mPM.removeUser(id); + } catch (RemoteException e) { + return false; + } + } + + /** + * @hide + */ + @Override + public void updateUserName(int id, String name) { + // TODO: + } + + /** + * @hide + */ + @Override + public void updateUserFlags(int id, int flags) { + // TODO: + } + private final ContextImpl mContext; private final IPackageManager mPM; diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index 850ad2b..dc0f529 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -223,6 +223,7 @@ public abstract class ApplicationThreadNative extends Binder { data.enforceInterface(IApplicationThread.descriptor); IBinder token = data.readStrongBinder(); + boolean taskRemoved = data.readInt() != 0; int startId = data.readInt(); int fl = data.readInt(); Intent args; @@ -231,7 +232,7 @@ public abstract class ApplicationThreadNative extends Binder } else { args = null; } - scheduleServiceArgs(token, startId, fl, args); + scheduleServiceArgs(token, taskRemoved, startId, fl, args); return true; } @@ -710,11 +711,12 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } - public final void scheduleServiceArgs(IBinder token, int startId, + public final void scheduleServiceArgs(IBinder token, boolean taskRemoved, int startId, int flags, Intent args) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeStrongBinder(token); + data.writeInt(taskRemoved ? 1 : 0); data.writeInt(startId); data.writeInt(flags); if (args != null) { diff --git a/core/java/android/app/FullBackupAgent.java b/core/java/android/app/FullBackupAgent.java deleted file mode 100644 index acd20bd..0000000 --- a/core/java/android/app/FullBackupAgent.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2009 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.app; - -import android.app.backup.BackupAgent; -import android.app.backup.BackupDataInput; -import android.app.backup.BackupDataOutput; -import android.app.backup.FileBackupHelper; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import java.io.File; -import java.util.ArrayList; -import java.util.LinkedList; - -/** - * Backs up an application's entire /data/data/<package>/... file system. This - * class is used by the desktop full backup mechanism and is not intended for direct - * use by applications. - * - * {@hide} - */ - -public class FullBackupAgent extends BackupAgent { - // !!! TODO: turn off debugging - private static final String TAG = "FullBackupAgent"; - private static final boolean DEBUG = true; - - @Override - public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, - ParcelFileDescriptor newState) { - LinkedList<File> dirsToScan = new LinkedList<File>(); - ArrayList<String> allFiles = new ArrayList<String>(); - - // build the list of files in the app's /data/data tree - dirsToScan.add(getFilesDir()); - if (DEBUG) Log.v(TAG, "Backing up dir tree @ " + getFilesDir().getAbsolutePath() + " :"); - while (dirsToScan.size() > 0) { - File dir = dirsToScan.removeFirst(); - File[] contents = dir.listFiles(); - if (contents != null) { - for (File f : contents) { - if (f.isDirectory()) { - dirsToScan.add(f); - } else if (f.isFile()) { - if (DEBUG) Log.v(TAG, " " + f.getAbsolutePath()); - allFiles.add(f.getAbsolutePath()); - } - } - } - } - - // That's the file set; now back it all up - FileBackupHelper helper = new FileBackupHelper(this, - allFiles.toArray(new String[allFiles.size()])); - helper.performBackup(oldState, data, newState); - } - - @Override - public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) { - } -} diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 24706f9..54c3422 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -133,7 +133,7 @@ public interface IActivityManager extends IInterface { IThumbnailReceiver receiver) throws RemoteException; public List<ActivityManager.RecentTaskInfo> getRecentTasks(int maxNum, int flags) throws RemoteException; - public Bitmap getTaskThumbnail(int taskId) throws RemoteException; + public ActivityManager.TaskThumbnails getTaskThumbnails(int taskId) throws RemoteException; public List getServices(int maxNum, int flags) throws RemoteException; public List<ActivityManager.ProcessErrorStateInfo> getProcessesInErrorState() throws RemoteException; @@ -347,6 +347,13 @@ public interface IActivityManager extends IInterface { public int getPackageScreenCompatMode(String packageName) throws RemoteException; public void setPackageScreenCompatMode(String packageName, int mode) throws RemoteException; + + // Multi-user APIs + public boolean switchUser(int userid) throws RemoteException; + + public boolean removeSubTask(int taskId, int subTaskIndex) throws RemoteException; + + public boolean removeTask(int taskId, int flags) throws RemoteException; /* * Private non-Binder interfaces @@ -521,7 +528,7 @@ public interface IActivityManager extends IInterface { int FORCE_STOP_PACKAGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+78; int KILL_PIDS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+79; int GET_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+80; - int GET_TASK_THUMBNAIL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+81; + int GET_TASK_THUMBNAILS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+81; int GET_RUNNING_APP_PROCESSES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+82; int GET_DEVICE_CONFIGURATION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+83; int PEEK_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+84; @@ -567,4 +574,7 @@ public interface IActivityManager extends IInterface { int SET_FRONT_ACTIVITY_SCREEN_COMPAT_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+124; int GET_PACKAGE_SCREEN_COMPAT_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+125; int SET_PACKAGE_SCREEN_COMPAT_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+126; + int SWITCH_USER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+127; + int REMOVE_SUB_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+128; + int REMOVE_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+129; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 93a8ff3..8c31559 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -78,8 +78,8 @@ public interface IApplicationThread extends IInterface { Intent intent, boolean rebind) throws RemoteException; void scheduleUnbindService(IBinder token, Intent intent) throws RemoteException; - void scheduleServiceArgs(IBinder token, int startId, int flags, Intent args) - throws RemoteException; + void scheduleServiceArgs(IBinder token, boolean taskRemoved, int startId, + int flags, Intent args) throws RemoteException; void scheduleStopService(IBinder token) throws RemoteException; static final int DEBUG_OFF = 0; static final int DEBUG_ON = 1; diff --git a/core/java/android/app/IBackupAgent.aidl b/core/java/android/app/IBackupAgent.aidl index fed2bc5..52fc623 100644 --- a/core/java/android/app/IBackupAgent.aidl +++ b/core/java/android/app/IBackupAgent.aidl @@ -51,6 +51,7 @@ oneway interface IBackupAgent { void doBackup(in ParcelFileDescriptor oldState, in ParcelFileDescriptor data, in ParcelFileDescriptor newState, + boolean storeApk, int token, IBackupManager callbackBinder); /** diff --git a/core/java/android/app/IThumbnailRetriever.aidl b/core/java/android/app/IThumbnailRetriever.aidl new file mode 100644 index 0000000..2a6737d --- /dev/null +++ b/core/java/android/app/IThumbnailRetriever.aidl @@ -0,0 +1,24 @@ +/* Copyright 2011, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ +package android.app; + +import android.graphics.Bitmap; + +/** + * System private API for retrieving thumbnails + */ +interface IThumbnailRetriever { + Bitmap getThumbnail(int index); +} diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index cd278be..7b02763 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -33,7 +33,6 @@ import android.os.Process; import android.os.SystemClock; import android.os.ServiceManager; import android.util.AndroidRuntimeException; -import android.util.Config; import android.util.Log; import android.view.IWindowManager; import android.view.KeyCharacterMap; diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 6541c54..4913e78 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -61,8 +61,7 @@ import android.util.Log; public class NotificationManager { private static String TAG = "NotificationManager"; - private static boolean DEBUG = false; - private static boolean localLOGV = DEBUG || android.util.Config.LOGV; + private static boolean localLOGV = false; private static INotificationManager sService; diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 05b9781..c179b35 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -371,6 +371,13 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac public static final int START_REDELIVER_INTENT = 3; /** + * Special constant for reporting that we are done processing + * {@link #onTaskRemoved(Intent)}. + * @hide + */ + public static final int START_TASK_REMOVED_COMPLETE = 1000; + + /** * This flag is set in {@link #onStartCommand} if the Intent is a * re-delivery of a previously delivered intent, because the service * had previously returned {@link #START_REDELIVER_INTENT} but had been @@ -500,6 +507,19 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac } /** + * This is called if the service is currently running and the user has + * removed a task that comes from the service's application. If you have + * set {@link android.content.pm.ServiceInfo#FLAG_STOP_WITH_TASK ServiceInfo.FLAG_STOP_WITH_TASK} + * then you will not receive this callback; instead, the service will simply + * be stopped. + * + * @param rootIntent The original root Intent that was used to launch + * the task that is being removed. + */ + public void onTaskRemoved(Intent rootIntent) { + } + + /** * Stop the service, if it was previously started. This is the same as * calling {@link android.content.Context#stopService} for this particular service. * diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 13a8b78..113c610 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -38,7 +38,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.util.DisplayMetrics; import android.util.Log; -import android.view.ViewRoot; +import android.view.ViewAncestor; import java.io.FileOutputStream; import java.io.IOException; @@ -632,7 +632,7 @@ public class WallpaperManager { public void setWallpaperOffsets(IBinder windowToken, float xOffset, float yOffset) { try { //Log.v(TAG, "Sending new wallpaper offsets from app..."); - ViewRoot.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( + ViewAncestor.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( windowToken, xOffset, yOffset, mWallpaperXStep, mWallpaperYStep); //Log.v(TAG, "...app returning after sending offsets!"); } catch (RemoteException e) { @@ -670,7 +670,7 @@ public class WallpaperManager { int x, int y, int z, Bundle extras) { try { //Log.v(TAG, "Sending new wallpaper offsets from app..."); - ViewRoot.getWindowSession(mContext.getMainLooper()).sendWallpaperCommand( + ViewAncestor.getWindowSession(mContext.getMainLooper()).sendWallpaperCommand( windowToken, action, x, y, z, extras, false); //Log.v(TAG, "...app returning after sending offsets!"); } catch (RemoteException e) { @@ -690,7 +690,7 @@ public class WallpaperManager { */ public void clearWallpaperOffsets(IBinder windowToken) { try { - ViewRoot.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( + ViewAncestor.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( windowToken, -1, -1, -1, -1); } catch (RemoteException e) { // Ignore. diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java index 29f8caf..473aec6 100644 --- a/core/java/android/app/admin/DeviceAdminReceiver.java +++ b/core/java/android/app/admin/DeviceAdminReceiver.java @@ -52,8 +52,7 @@ import android.os.Bundle; */ public class DeviceAdminReceiver extends BroadcastReceiver { private static String TAG = "DevicePolicy"; - private static boolean DEBUG = false; - private static boolean localLOGV = DEBUG || android.util.Config.LOGV; + private static boolean localLOGV = false; /** * This is the primary action that a device administrator must implement to be diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index cb4e0e7..dc60e24 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -85,7 +85,7 @@ import java.io.IOException; */ public abstract class BackupAgent extends ContextWrapper { private static final String TAG = "BackupAgent"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; public BackupAgent() { super(null); @@ -172,11 +172,18 @@ public abstract class BackupAgent extends ContextWrapper { * @param newState An open, read/write ParcelFileDescriptor pointing to an * empty file. The application should record the final backup * state here after restoring its data from the <code>data</code> stream. + * When a full-backup dataset is being restored, this will be <code>null</code>. */ public abstract void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException; + /** + * Package-private, used only for dispatching an extra step during full backup + */ + void onSaveApk(BackupDataOutput data) { + if (DEBUG) Log.v(TAG, "--- base onSaveApk() ---"); + } // ----- Core implementation ----- @@ -199,12 +206,18 @@ public abstract class BackupAgent extends ContextWrapper { public void doBackup(ParcelFileDescriptor oldState, ParcelFileDescriptor data, ParcelFileDescriptor newState, + boolean storeApk, int token, IBackupManager callbackBinder) throws RemoteException { // Ensure that we're running with the app's normal permission level long ident = Binder.clearCallingIdentity(); if (DEBUG) Log.v(TAG, "doBackup() invoked"); BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor()); + + if (storeApk) { + onSaveApk(output); + } + try { BackupAgent.this.onBackup(oldState, output, newState); } catch (IOException ex) { diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java new file mode 100644 index 0000000..9850566 --- /dev/null +++ b/core/java/android/app/backup/FullBackup.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +/** + * Global constant definitions et cetera related to the full-backup-to-fd + * binary format. + * + * @hide + */ +public class FullBackup { + public static String APK_TREE_TOKEN = "a"; + public static String OBB_TREE_TOKEN = "obb"; + public static String ROOT_TREE_TOKEN = "r"; + public static String DATA_TREE_TOKEN = "f"; + public static String DATABASE_TREE_TOKEN = "db"; + public static String SHAREDPREFS_TREE_TOKEN = "sp"; + public static String CACHE_TREE_TOKEN = "c"; + + public static String FULL_BACKUP_INTENT_ACTION = "fullback"; + public static String FULL_RESTORE_INTENT_ACTION = "fullrest"; + public static String CONF_TOKEN_INTENT_EXTRA = "conftoken"; + + static public native int backupToTar(String packageName, String domain, + String linkdomain, String rootpath, String path, BackupDataOutput output); +} diff --git a/core/java/android/app/backup/FullBackupAgent.java b/core/java/android/app/backup/FullBackupAgent.java new file mode 100644 index 0000000..f0a1f2a --- /dev/null +++ b/core/java/android/app/backup/FullBackupAgent.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2009 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.app.backup; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import libcore.io.Libcore; +import libcore.io.ErrnoException; +import libcore.io.OsConstants; +import libcore.io.StructStat; + +import java.io.File; +import java.util.HashSet; +import java.util.LinkedList; + +/** + * Backs up an application's entire /data/data/<package>/... file system. This + * class is used by the desktop full backup mechanism and is not intended for direct + * use by applications. + * + * {@hide} + */ + +public class FullBackupAgent extends BackupAgent { + // !!! TODO: turn off debugging + private static final String TAG = "FullBackupAgent"; + private static final boolean DEBUG = true; + + PackageManager mPm; + + private String mMainDir; + private String mFilesDir; + private String mDatabaseDir; + private String mSharedPrefsDir; + private String mCacheDir; + private String mLibDir; + + @Override + public void onCreate() { + mPm = getPackageManager(); + try { + ApplicationInfo appInfo = mPm.getApplicationInfo(getPackageName(), 0); + mMainDir = new File(appInfo.dataDir).getAbsolutePath(); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable to find package " + getPackageName()); + throw new RuntimeException(e); + } + + mFilesDir = getFilesDir().getAbsolutePath(); + mDatabaseDir = getDatabasePath("foo").getParentFile().getAbsolutePath(); + mSharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getAbsolutePath(); + mCacheDir = getCacheDir().getAbsolutePath(); + + ApplicationInfo app = getApplicationInfo(); + mLibDir = (app.nativeLibraryDir != null) + ? new File(app.nativeLibraryDir).getAbsolutePath() + : null; + } + + @Override + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) { + // Filters, the scan queue, and the set of resulting entities + HashSet<String> filterSet = new HashSet<String>(); + + // Okay, start with the app's root tree, but exclude all of the canonical subdirs + if (mLibDir != null) { + filterSet.add(mLibDir); + } + filterSet.add(mCacheDir); + filterSet.add(mDatabaseDir); + filterSet.add(mSharedPrefsDir); + filterSet.add(mFilesDir); + processTree(FullBackup.ROOT_TREE_TOKEN, mMainDir, filterSet, data); + + // Now do the same for the files dir, db dir, and shared prefs dir + filterSet.add(mMainDir); + filterSet.remove(mFilesDir); + processTree(FullBackup.DATA_TREE_TOKEN, mFilesDir, filterSet, data); + + filterSet.add(mFilesDir); + filterSet.remove(mDatabaseDir); + processTree(FullBackup.DATABASE_TREE_TOKEN, mDatabaseDir, filterSet, data); + + filterSet.add(mDatabaseDir); + filterSet.remove(mSharedPrefsDir); + processTree(FullBackup.SHAREDPREFS_TREE_TOKEN, mSharedPrefsDir, filterSet, data); + } + + private void processTree(String domain, String rootPath, + HashSet<String> excludes, BackupDataOutput data) { + // Scan the dir tree (if it actually exists) and process each entry we find + File rootFile = new File(rootPath); + if (rootFile.exists()) { + LinkedList<File> scanQueue = new LinkedList<File>(); + scanQueue.add(rootFile); + + while (scanQueue.size() > 0) { + File file = scanQueue.remove(0); + String filePath = file.getAbsolutePath(); + + // prune this subtree? + if (excludes.contains(filePath)) { + continue; + } + + // If it's a directory, enqueue its contents for scanning. + try { + StructStat stat = Libcore.os.lstat(filePath); + if (OsConstants.S_ISLNK(stat.st_mode)) { + if (DEBUG) Log.i(TAG, "Symlink (skipping)!: " + file); + continue; + } else if (OsConstants.S_ISDIR(stat.st_mode)) { + File[] contents = file.listFiles(); + if (contents != null) { + for (File entry : contents) { + scanQueue.add(0, entry); + } + } + } + } catch (ErrnoException e) { + if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e); + continue; + } + + // Finally, back this file up before proceeding + FullBackup.backupToTar(getPackageName(), domain, null, rootPath, filePath, data); + } + } + } + + @Override + void onSaveApk(BackupDataOutput data) { + ApplicationInfo app = getApplicationInfo(); + if (DEBUG) Log.i(TAG, "APK flags: system=" + ((app.flags & ApplicationInfo.FLAG_SYSTEM) != 0) + + " updated=" + ((app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) + + " locked=" + ((app.flags & ApplicationInfo.FLAG_FORWARD_LOCK) != 0) ); + if (DEBUG) Log.i(TAG, "codepath: " + getPackageCodePath()); + + // Forward-locked apps, system-bundled .apks, etc are filtered out before we get here + final String pkgName = getPackageName(); + final String apkDir = new File(getPackageCodePath()).getParent(); + FullBackup.backupToTar(pkgName, FullBackup.APK_TREE_TOKEN, null, + apkDir, getPackageCodePath(), data); + + // Save associated .obb content if it exists and we did save the apk + // check for .obb and save those too + final File obbDir = Environment.getExternalStorageAppObbDirectory(pkgName); + if (obbDir != null) { + if (DEBUG) Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath()); + File[] obbFiles = obbDir.listFiles(); + if (obbFiles != null) { + final String obbDirName = obbDir.getAbsolutePath(); + for (File obb : obbFiles) { + FullBackup.backupToTar(pkgName, FullBackup.OBB_TREE_TOKEN, null, + obbDirName, obb.getAbsolutePath(), data); + } + } + } + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) { + } +} diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index b315b3a..94e31a8 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -16,7 +16,9 @@ package android.app.backup; +import android.app.backup.IFullBackupRestoreObserver; import android.app.backup.IRestoreSession; +import android.os.ParcelFileDescriptor; import android.content.Intent; /** @@ -121,6 +123,42 @@ interface IBackupManager { void backupNow(); /** + * Write a full backup of the given package to the supplied file descriptor. + * The fd may be a socket or other non-seekable destination. If no package names + * are supplied, then every application on the device will be backed up to the output. + * + * <p>This method is <i>synchronous</i> -- it does not return until the backup has + * completed. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + * + * @param fd The file descriptor to which a 'tar' file stream is to be written + * @param includeApks If <code>true</code>, the resulting tar stream will include the + * application .apk files themselves as well as their data. + * @param includeShared If <code>true</code>, the resulting tar stream will include + * the contents of the device's shared storage (SD card or equivalent). + * @param allApps If <code>true</code>, the resulting tar stream will include all + * installed applications' data, not just those named in the <code>packageNames</code> + * parameter. + * @param packageNames The package names of the apps whose data (and optionally .apk files) + * are to be backed up. The <code>allApps</code> parameter supersedes this. + */ + void fullBackup(in ParcelFileDescriptor fd, boolean includeApks, boolean includeShared, + boolean allApps, in String[] packageNames); + + /** + * Confirm that the requested full backup/restore operation can proceed. The system will + * not actually perform the operation described to fullBackup() / fullRestore() unless the + * UI calls back into the Backup Manager to confirm, passing the correct token. At + * the same time, the UI supplies a callback Binder for progress notifications during + * the operation. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + */ + void acknowledgeFullBackupOrRestore(int token, boolean allow, + IFullBackupRestoreObserver observer); + + /** * Identify the currently selected transport. Callers must hold the * android.permission.BACKUP permission to use this method. */ diff --git a/core/java/android/app/backup/IFullBackupRestoreObserver.aidl b/core/java/android/app/backup/IFullBackupRestoreObserver.aidl new file mode 100644 index 0000000..3e0b73d --- /dev/null +++ b/core/java/android/app/backup/IFullBackupRestoreObserver.aidl @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +/** + * Observer of a full backup or restore process. The observer is told "interesting" + * information about an ongoing full backup or restore action. + * + * {@hide} + */ + +oneway interface IFullBackupRestoreObserver { + /** + * Notification: a full backup operation has begun. + */ + void onStartBackup(); + + /** + * Notification: the system has begun backing up the given package. + * + * @param name The name of the application being saved. This will typically be a + * user-meaningful name such as "Browser" rather than a package name such as + * "com.android.browser", though this is not guaranteed. + */ + void onBackupPackage(String name); + + /** + * Notification: the full backup operation has ended. + */ + void onEndBackup(); + + /** + * Notification: a restore-from-full-backup operation has begun. + */ + void onStartRestore(); + + /** + * Notification: the system has begun restore of the given package. + * + * @param name The name of the application being saved. This will typically be a + * user-meaningful name such as "Browser" rather than a package name such as + * "com.android.browser", though this is not guaranteed. + */ + void onRestorePackage(String name); + + /** + * Notification: the restore-from-full-backup operation has ended. + */ + void onEndRestore(); + + /** + * The user's window of opportunity for confirming the operation has timed out. + */ + void onTimeout(); +} diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index fa55520..8a9bef0 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -603,7 +603,7 @@ public final class BluetoothHeadset implements BluetoothProfile { */ public boolean setAudioState(BluetoothDevice device, int state) { if (DBG) log("setAudioState"); - if (mService != null && isEnabled()) { + if (mService != null && !isDisabled()) { try { return mService.setAudioState(device, state); } catch (RemoteException e) {Log.e(TAG, e.toString());} @@ -622,7 +622,7 @@ public final class BluetoothHeadset implements BluetoothProfile { */ public int getAudioState(BluetoothDevice device) { if (DBG) log("getAudioState"); - if (mService != null && isEnabled()) { + if (mService != null && !isDisabled()) { try { return mService.getAudioState(device); } catch (RemoteException e) {Log.e(TAG, e.toString());} @@ -705,6 +705,11 @@ public final class BluetoothHeadset implements BluetoothProfile { return false; } + private boolean isDisabled() { + if (mAdapter.getState() == BluetoothAdapter.STATE_OFF) return true; + return false; + } + private boolean isValidDevice(BluetoothDevice device) { if (device == null) return false; diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 2d03e7c..364821e 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -35,7 +35,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.text.TextUtils; -import android.util.Config; import android.util.EventLog; import android.util.Log; @@ -1627,9 +1626,9 @@ public abstract class ContentResolver { return sContentService; } IBinder b = ServiceManager.getService(CONTENT_SERVICE_NAME); - if (Config.LOGV) Log.v("ContentService", "default service binder = " + b); + if (false) Log.v("ContentService", "default service binder = " + b); sContentService = IContentService.Stub.asInterface(b); - if (Config.LOGV) Log.v("ContentService", "default service = " + sContentService); + if (false) Log.v("ContentService", "default service = " + sContentService); return sContentService; } diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java index afe8483..a2af558 100644 --- a/core/java/android/content/ContentService.java +++ b/core/java/android/content/ContentService.java @@ -25,7 +25,6 @@ import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.ServiceManager; -import android.util.Config; import android.util.Log; import android.Manifest; @@ -104,7 +103,7 @@ public final class ContentService extends IContentService.Stub { } synchronized (mRootNode) { mRootNode.addObserverLocked(uri, observer, notifyForDescendents, mRootNode); - if (Config.LOGV) Log.v(TAG, "Registered observer " + observer + " at " + uri + + if (false) Log.v(TAG, "Registered observer " + observer + " at " + uri + " with notifyForDescendents " + notifyForDescendents); } } @@ -115,7 +114,7 @@ public final class ContentService extends IContentService.Stub { } synchronized (mRootNode) { mRootNode.removeObserverLocked(observer); - if (Config.LOGV) Log.v(TAG, "Unregistered observer " + observer); + if (false) Log.v(TAG, "Unregistered observer " + observer); } } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 7bdd1b9..3d637e9 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1878,7 +1878,7 @@ public class Intent implements Parcelable, Cloneable { "android.intent.action.USB_ANLG_HEADSET_PLUG"; /** - * Broadcast Action: An analog audio speaker/headset plugged in or unplugged. + * Broadcast Action: A digital audio speaker/headset plugged in or unplugged. * * <p>The intent will have the following extra values: * <ul> @@ -1908,6 +1908,21 @@ public class Intent implements Parcelable, Cloneable { "android.intent.action.HDMI_AUDIO_PLUG"; /** + * <p>Broadcast Action: The user has switched on advanced settings in the settings app:</p> + * <ul> + * <li><em>state</em> - A boolean value indicating whether the settings is on or off.</li> + * </ul> + * + * <p class="note">This is a protected intent that can only be sent + * by the system. + * + * @hide + */ + //@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_ADVANCED_SETTINGS_CHANGED + = "android.intent.action.ADVANCED_SETTINGS"; + + /** * Broadcast Action: An outgoing call is about to be placed. * * <p>The Intent will have the following extra value: diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index 06c1ecb..2a0ebcf 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -31,7 +31,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.PatternMatcher; import android.util.AndroidException; -import android.util.Config; import android.util.Log; import android.util.Printer; @@ -670,7 +669,7 @@ public class IntentFilter implements Parcelable { if (host == null) { return NO_MATCH_DATA; } - if (Config.LOGV) Log.v("IntentFilter", + if (false) Log.v("IntentFilter", "Match host " + host + ": " + mHost); if (mWild) { if (host.length() < mHost.length()) { @@ -1095,14 +1094,14 @@ public class IntentFilter implements Parcelable { public final int match(String action, String type, String scheme, Uri data, Set<String> categories, String logTag) { if (action != null && !matchAction(action)) { - if (Config.LOGV) Log.v( + if (false) Log.v( logTag, "No matching action " + action + " for " + this); return NO_MATCH_ACTION; } int dataMatch = matchData(type, scheme, data); if (dataMatch < 0) { - if (Config.LOGV) { + if (false) { if (dataMatch == NO_MATCH_TYPE) { Log.v(logTag, "No matching type " + type + " for " + this); @@ -1117,7 +1116,7 @@ public class IntentFilter implements Parcelable { String categoryMismatch = matchCategories(categories); if (categoryMismatch != null) { - if (Config.LOGV) { + if (false) { Log.v(logTag, "No matching category " + categoryMismatch + " for " + this); } return NO_MATCH_CATEGORY; diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 92b2c3b..4b38d48 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -89,7 +89,16 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * <p>If android:allowBackup is set to false, this attribute is ignored. */ public String backupAgentName; - + + /** + * Class implementing the package's *full* backup functionality. This + * is not usable except by system-installed packages. It can be the same + * as the backupAgent. + * + * @hide + */ + public String fullBackupAgentName; + /** * Value for {@link #flags}: if set, this application is installed in the * device's system image. @@ -505,6 +514,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(installLocation); dest.writeString(manageSpaceActivityName); dest.writeString(backupAgentName); + dest.writeString(fullBackupAgentName); dest.writeInt(descriptionRes); } @@ -538,6 +548,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { installLocation = source.readInt(); manageSpaceActivityName = source.readString(); backupAgentName = source.readString(); + fullBackupAgentName = source.readString(); descriptionRes = source.readInt(); } diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 20b1b50..37b6822 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -36,6 +36,7 @@ import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; import android.net.Uri; import android.content.IntentSender; @@ -342,4 +343,7 @@ interface IPackageManager { boolean setInstallLocation(int loc); int getInstallLocation(); + + UserInfo createUser(in String name, int flags); + boolean removeUser(int userId); } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 80bed0d..ff817c1 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -150,21 +150,21 @@ public abstract class PackageManager { * {@link PackageInfo#permissions}. */ public static final int GET_PERMISSIONS = 0x00001000; - + /** * Flag parameter to retrieve all applications(even uninstalled ones) with data directories. - * This state could have resulted if applications have been deleted with flag + * This state could have resulted if applications have been deleted with flag * DONT_DELETE_DATA * with a possibility of being replaced or reinstalled in future */ public static final int GET_UNINSTALLED_PACKAGES = 0x00002000; - + /** * {@link PackageInfo} flag: return information about * hardware preferences in * {@link PackageInfo#configPreferences PackageInfo.configPreferences} and * requested features in {@link PackageInfo#reqFeatures - * PackageInfo.reqFeatures}. + * PackageInfo.reqFeatures}. */ public static final int GET_CONFIGURATIONS = 0x00004000; @@ -244,7 +244,7 @@ public abstract class PackageManager { public static final int INSTALL_REPLACE_EXISTING = 0x00000002; /** - * Flag parameter for {@link #installPackage} to indicate that you want to + * Flag parameter for {@link #installPackage} to indicate that you want to * allow test packages (those that have set android:testOnly in their * manifest) to be installed. * @hide @@ -555,7 +555,7 @@ public abstract class PackageManager { * Return code for when package deletion succeeds. This is passed to the * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system * succeeded in deleting the package. - * + * * @hide */ public static final int DELETE_SUCCEEDED = 1; @@ -564,7 +564,7 @@ public abstract class PackageManager { * Deletion failed return code: this is passed to the * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system * failed to delete the package for an unspecified reason. - * + * * @hide */ public static final int DELETE_FAILED_INTERNAL_ERROR = -1; @@ -574,7 +574,7 @@ public abstract class PackageManager { * {@link IPackageDeleteObserver} by {@link #deletePackage()} if the system * failed to delete the package because it is the active DevicePolicy * manager. - * + * * @hide */ public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; @@ -583,7 +583,7 @@ public abstract class PackageManager { * Return code that is passed to the {@link IPackageMoveObserver} by * {@link #movePackage(android.net.Uri, IPackageMoveObserver)} when the * package has been successfully moved by the system. - * + * * @hide */ public static final int MOVE_SUCCEEDED = 1; @@ -641,7 +641,7 @@ public abstract class PackageManager { * {@link #movePackage(android.net.Uri, IPackageMoveObserver)} if the * specified package already has an operation pending in the * {@link PackageHandler} queue. - * + * * @hide */ public static final int MOVE_FAILED_OPERATION_PENDING = -7; @@ -662,10 +662,15 @@ public abstract class PackageManager { public static final int MOVE_EXTERNAL_MEDIA = 0x00000002; /** - * Feature for {@link #getSystemAvailableFeatures} and - * {@link #hasSystemFeature}: The device's audio pipeline is low-latency, - * more suitable for audio applications sensitive to delays or lag in - * sound input or output. + * Range of IDs allocated for a user. + * @hide + */ + public static final int PER_USER_RANGE = 100000; + + /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: The device's + * audio pipeline is low-latency, more suitable for audio applications sensitive to delays or + * lag in sound input or output. */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_AUDIO_LOW_LATENCY = "android.hardware.audio.low_latency"; @@ -789,7 +794,7 @@ public abstract class PackageManager { */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_SENSOR_PROXIMITY = "android.hardware.sensor.proximity"; - + /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device has a telephony radio with data @@ -797,14 +802,14 @@ public abstract class PackageManager { */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_TELEPHONY = "android.hardware.telephony"; - + /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device has a CDMA telephony stack. */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_TELEPHONY_CDMA = "android.hardware.telephony.cdma"; - + /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device has a GSM telephony stack. @@ -847,8 +852,8 @@ public abstract class PackageManager { */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_TOUCHSCREEN = "android.hardware.touchscreen"; - - + + /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device's touch screen supports @@ -856,7 +861,7 @@ public abstract class PackageManager { */ @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_TOUCHSCREEN_MULTITOUCH = "android.hardware.touchscreen.multitouch"; - + /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device's touch screen is capable of @@ -932,11 +937,11 @@ public abstract class PackageManager { * @return Returns a PackageInfo object containing information about the package. * If flag GET_UNINSTALLED_PACKAGES is set and if the package is not * found in the list of installed applications, the package information is - * retrieved from the list of uninstalled applications(which includes + * retrieved from the list of uninstalled applications(which includes * installed applications as well as applications * with data directory ie applications which had been * deleted with DONT_DELTE_DATA flag set). - * + * * @see #GET_ACTIVITIES * @see #GET_GIDS * @see #GET_CONFIGURATIONS @@ -947,7 +952,7 @@ public abstract class PackageManager { * @see #GET_SERVICES * @see #GET_SIGNATURES * @see #GET_UNINSTALLED_PACKAGES - * + * */ public abstract PackageInfo getPackageInfo(String packageName, int flags) throws NameNotFoundException; @@ -960,7 +965,7 @@ public abstract class PackageManager { * the canonical name for each package. */ public abstract String[] currentToCanonicalPackageNames(String[] names); - + /** * Map from a packages canonical name to the current name in use on the device. * @param names Array of new names to be mapped. @@ -968,7 +973,7 @@ public abstract class PackageManager { * the current name for each package. */ public abstract String[] canonicalToCurrentPackageNames(String[] names); - + /** * 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 @@ -976,12 +981,12 @@ public abstract class PackageManager { * 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. @@ -1077,16 +1082,16 @@ public abstract class PackageManager { * * @param packageName The full name (i.e. com.google.apps.contacts) of an * application. - * @param flags Additional option flags. Use any combination of + * @param flags Additional option flags. Use any combination of * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, * {@link #GET_UNINSTALLED_PACKAGES} to modify the data returned. * - * @return {@link ApplicationInfo} Returns ApplicationInfo object containing + * @return {@link ApplicationInfo} Returns ApplicationInfo object containing * information about the package. * If flag GET_UNINSTALLED_PACKAGES is set and if the package is not - * found in the list of installed applications, - * the application information is retrieved from the - * list of uninstalled applications(which includes + * found in the list of installed applications, + * the application information is retrieved from the + * list of uninstalled applications(which includes * installed applications as well as applications * with data directory ie applications which had been * deleted with DONT_DELTE_DATA flag set). @@ -1108,7 +1113,7 @@ public abstract class PackageManager { * @param component The full component name (i.e. * com.google.apps.contacts/com.google.apps.contacts.ContactsList) of an Activity * class. - * @param flags Additional option flags. Use any combination of + * @param flags Additional option flags. Use any combination of * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, * to modify the data (in ApplicationInfo) returned. * @@ -1131,7 +1136,7 @@ public abstract class PackageManager { * @param component The full component name (i.e. * com.google.apps.calendar/com.google.apps.calendar.CalendarAlarm) of a Receiver * class. - * @param flags Additional option flags. Use any combination of + * @param flags Additional option flags. Use any combination of * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, * to modify the data returned. * @@ -1154,12 +1159,12 @@ public abstract class PackageManager { * @param component The full component name (i.e. * com.google.apps.media/com.google.apps.media.BackgroundPlayback) of a Service * class. - * @param flags Additional option flags. Use any combination of + * @param flags Additional option flags. Use any combination of * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, * to modify the data returned. * * @return ServiceInfo containing information about the service. - * + * * @see #GET_META_DATA * @see #GET_SHARED_LIBRARY_FILES */ @@ -1206,7 +1211,7 @@ public abstract class PackageManager { * * @return A List of PackageInfo objects, one for each package that is * installed on the device. In the unlikely case of there being no - * installed packages, an empty list is returned. + * installed packages, an empty list is returned. * If flag GET_UNINSTALLED_PACKAGES is set, a list of all * applications including those deleted with DONT_DELETE_DATA * (partially installed apps with data directory) will be returned. @@ -1221,7 +1226,7 @@ public abstract class PackageManager { * @see #GET_SERVICES * @see #GET_SIGNATURES * @see #GET_UNINSTALLED_PACKAGES - * + * */ public abstract List<PackageInfo> getInstalledPackages(int flags); @@ -1283,7 +1288,7 @@ public abstract class PackageManager { * the device is rebooted before it is written. */ public abstract boolean addPermissionAsync(PermissionInfo info); - + /** * Removes a permission that was previously added with * {@link #addPermission(PermissionInfo)}. The same ownership rules apply @@ -1370,7 +1375,7 @@ public abstract class PackageManager { * user id is not currently assigned. */ public abstract String getNameForUid(int uid); - + /** * Return the user id associated with a shared user name. Multiple * applications can specify a shared user name in their manifest and thus @@ -1391,38 +1396,38 @@ public abstract class PackageManager { * device. If flag GET_UNINSTALLED_PACKAGES has been set, a list of all * applications including those deleted with DONT_DELETE_DATA(partially * installed apps with data directory) will be returned. - * - * @param flags Additional option flags. Use any combination of + * + * @param flags Additional option flags. Use any combination of * {@link #GET_META_DATA}, {@link #GET_SHARED_LIBRARY_FILES}, * {link #GET_UNINSTALLED_PACKAGES} to modify the data returned. * * @return A List of ApplicationInfo objects, one for each application that * is installed on the device. In the unlikely case of there being - * no installed applications, an empty list is returned. + * no installed applications, an empty list is returned. * If flag GET_UNINSTALLED_PACKAGES is set, a list of all * applications including those deleted with DONT_DELETE_DATA * (partially installed apps with data directory) will be returned. - * + * * @see #GET_META_DATA * @see #GET_SHARED_LIBRARY_FILES * @see #GET_UNINSTALLED_PACKAGES */ public abstract List<ApplicationInfo> getInstalledApplications(int flags); - + /** * Get a list of shared libraries that are available on the * system. - * + * * @return An array of shared library names that are * available on the system, or null if none are installed. - * + * */ public abstract String[] getSystemSharedLibraryNames(); /** * Get a list of features that are available on the * system. - * + * * @return An array of FeatureInfo classes describing the features * that are available on the system, or null if there are none(!!). */ @@ -1431,7 +1436,7 @@ public abstract class PackageManager { /** * Check whether the given feature name is one of the available * features as returned by {@link #getSystemAvailableFeatures()}. - * + * * @return Returns true if the devices supports the feature, else * false. */ @@ -1448,7 +1453,7 @@ public abstract class PackageManager { * that {@link android.content.Context#startActivity(Intent)} and * {@link android.content.Intent#resolveActivity(PackageManager) * Intent.resolveActivity(PackageManager)} do.</p> - * + * * @param intent An intent containing all of the desired specification * (action, data, type, category, and/or component). * @param flags Additional option flags. The most important is @@ -1747,7 +1752,7 @@ public abstract class PackageManager { * * @return Returns the image of the logo or null if the activity has no * logo specified. - * + * * @throws NameNotFoundException Thrown if the resources for the given * activity could not be loaded. * @@ -1768,7 +1773,7 @@ public abstract class PackageManager { * * @return Returns the image of the logo, or null if the activity has no * logo specified. - * + * * @throws NameNotFoundException Thrown if the resources for application * matching the given intent could not be loaded. * @@ -1801,7 +1806,7 @@ public abstract class PackageManager { * * @return Returns the image of the logo, or null if no application logo * has been specified. - * + * * @throws NameNotFoundException Thrown if the resources for the given * application could not be loaded. * @@ -1935,7 +1940,7 @@ public abstract class PackageManager { * @see #GET_RECEIVERS * @see #GET_SERVICES * @see #GET_SIGNATURES - * + * */ public PackageInfo getPackageArchiveInfo(String archiveFilePath, int flags) { PackageParser packageParser = new PackageParser(archiveFilePath); @@ -1952,7 +1957,7 @@ public abstract class PackageManager { /** * @hide - * + * * Install a package. Since this may take a little while, the result will * be posted back to the given observer. An installation will fail if the calling context * lacks the {@link android.Manifest.permission#INSTALL_PACKAGES} permission, if the @@ -2012,11 +2017,11 @@ public abstract class PackageManager { /** * Retrieve the package name of the application that installed a package. This identifies * which market the package came from. - * + * * @param packageName The name of the package to query */ public abstract String getInstallerPackageName(String packageName); - + /** * Attempts to clear the user data directory of an application. * Since this may take a little while, the result will @@ -2071,7 +2076,7 @@ public abstract class PackageManager { * of bytes if possible. * @param observer call back used to notify when * the operation is completed - * + * * @hide */ public abstract void freeStorageAndNotify(long freeStorageSize, IPackageDataObserver observer); @@ -2096,7 +2101,7 @@ public abstract class PackageManager { * @param pi IntentSender call back used to * notify when the operation is completed.May be null * to indicate that no call back is desired. - * + * * @hide */ public abstract void freeStorage(long freeStorageSize, IntentSender pi); @@ -2172,7 +2177,7 @@ public abstract class PackageManager { * @deprecated This is a protected API that should not have been available * to third party applications. It is the platform's responsibility for * assigning preferred activities and this can not be directly modified. - * + * * Add a new preferred activity mapping to the system. This will be used * to automatically select the given activity component when * {@link Context#startActivity(Intent) Context.startActivity()} finds @@ -2195,7 +2200,7 @@ public abstract class PackageManager { * @deprecated This is a protected API that should not have been available * to third party applications. It is the platform's responsibility for * assigning preferred activities and this can not be directly modified. - * + * * Replaces an existing preferred activity mapping to the system, and if that were not present * adds a new preferred activity. This will be used * to automatically select the given activity component when @@ -2304,7 +2309,7 @@ public abstract class PackageManager { */ public abstract void setApplicationEnabledSetting(String packageName, int newState, int flags); - + /** * Return the the enabled setting for an application. This returns * the last value set by @@ -2345,4 +2350,79 @@ public abstract class PackageManager { */ public abstract void movePackage( String packageName, IPackageMoveObserver observer, int flags); + + /** + * Creates a user with the specified name and options. + * + * @param name the user's name + * @param flags flags that identify the type of user and other properties. + * @see UserInfo + * + * @return the UserInfo object for the created user, or null if the user could not be created. + * @hide + */ + public abstract UserInfo createUser(String name, int flags); + + /** + * @return the list of users that were created + * @hide + */ + public abstract List<UserInfo> getUsers(); + + /** + * @param id the ID of the user, where 0 is the primary user. + * @hide + */ + public abstract boolean removeUser(int id); + + /** + * Updates the user's name. + * + * @param id the user's id + * @param name the new name for the user + * @hide + */ + public abstract void updateUserName(int id, String name); + + /** + * Changes the user's properties specified by the flags. + * + * @param id the user's id + * @param flags the new flags for the user + * @hide + */ + public abstract void updateUserFlags(int id, int flags); + + /** + * Checks to see if the user id is the same for the two uids, i.e., they belong to the same + * user. + * @hide + */ + public static boolean isSameUser(int uid1, int uid2) { + return getUserId(uid1) == getUserId(uid2); + } + + /** + * Returns the user id for a given uid. + * @hide + */ + public static int getUserId(int uid) { + return uid / PER_USER_RANGE; + } + + /** + * Returns the uid that is composed from the userId and the appId. + * @hide + */ + public static int getUid(int userId, int appId) { + return userId * PER_USER_RANGE + (appId % PER_USER_RANGE); + } + + /** + * Returns the app id (or base uid) for a given uid, stripping out the user id from it. + * @hide + */ + public static int getAppId(int uid) { + return uid % PER_USER_RANGE; + } } diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 10799a4..9ff324b 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -24,11 +24,11 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.PatternMatcher; import android.util.AttributeSet; -import android.util.Config; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; @@ -384,7 +384,7 @@ public class PackageParser { return null; } - if ((flags&PARSE_CHATTY) != 0 && Config.LOGD) Log.d( + if ((flags&PARSE_CHATTY) != 0 && false) Log.d( TAG, "Scanning package: " + mArchiveSourcePath); XmlResourceParser parser = null; @@ -666,7 +666,7 @@ public class PackageParser { outError[0] = "No start tag found"; return null; } - if ((flags&PARSE_CHATTY) != 0 && Config.LOGV) Log.v( + if ((flags&PARSE_CHATTY) != 0 && false) Log.v( TAG, "Root element name: '" + parser.getName() + "'"); if (!parser.getName().equals("manifest")) { outError[0] = "No <manifest> tag"; @@ -701,7 +701,7 @@ public class PackageParser { outError[0] = "No start tag found"; return null; } - if ((flags&PARSE_CHATTY) != 0 && Config.LOGV) Log.v( + if ((flags&PARSE_CHATTY) != 0 && false) Log.v( TAG, "Root element name: '" + parser.getName() + "'"); if (!parser.getName().equals("manifest")) { outError[0] = "No <manifest> tag"; @@ -1506,7 +1506,17 @@ public class PackageParser { } } } - + + name = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestApplication_fullBackupAgent, 0); + if (name != null) { + ai.fullBackupAgentName = buildClassName(pkgName, name, outError); + if (true) { + Log.v(TAG, "android:fullBackupAgent=" + ai.fullBackupAgentName + + " from " + pkgName + "+" + name); + } + } + TypedValue v = sa.peekValue( com.android.internal.R.styleable.AndroidManifestApplication_label); if (v != null && (ai.labelRes=v.resourceId) == 0) { @@ -2475,6 +2485,13 @@ public class PackageParser { s.info.permission = str.length() > 0 ? str.toString().intern() : null; } + s.info.flags = 0; + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestService_stopWithTask, + false)) { + s.info.flags |= ServiceInfo.FLAG_STOP_WITH_TASK; + } + sa.recycle(); if ((owner.applicationInfo.flags&ApplicationInfo.FLAG_CANT_SAVE_STATE) != 0) { diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 087a4fe..612e345 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -33,17 +33,35 @@ public class ServiceInfo extends ComponentInfo */ public String permission; + /** + * Bit in {@link #flags}: If set, the service will automatically be + * stopped by the system if the user removes a task that is rooted + * in one of the application's activities. Set from the + * {@link android.R.attr#stopWithTask} attribute. + */ + public static final int FLAG_STOP_WITH_TASK = 0x0001; + + /** + * Options that have been set in the service declaration in the + * manifest. + * These include: + * {@link #FLAG_STOP_WITH_TASK} + */ + public int flags; + public ServiceInfo() { } public ServiceInfo(ServiceInfo orig) { super(orig); permission = orig.permission; + flags = orig.flags; } public void dump(Printer pw, String prefix) { super.dumpFront(pw, prefix); pw.println(prefix + "permission=" + permission); + pw.println(prefix + "flags=0x" + Integer.toHexString(flags)); } public String toString() { @@ -59,6 +77,7 @@ public class ServiceInfo extends ComponentInfo public void writeToParcel(Parcel dest, int parcelableFlags) { super.writeToParcel(dest, parcelableFlags); dest.writeString(permission); + dest.writeInt(flags); } public static final Creator<ServiceInfo> CREATOR = @@ -74,5 +93,6 @@ public class ServiceInfo extends ComponentInfo private ServiceInfo(Parcel source) { super(source); permission = source.readString(); + flags = source.readInt(); } } diff --git a/core/java/android/content/pm/UserInfo.aidl b/core/java/android/content/pm/UserInfo.aidl new file mode 100644 index 0000000..2e7cb8f --- /dev/null +++ b/core/java/android/content/pm/UserInfo.aidl @@ -0,0 +1,20 @@ +/* +** +** Copyright 2011, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.content.pm; + +parcelable UserInfo; diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java new file mode 100644 index 0000000..ba5331c --- /dev/null +++ b/core/java/android/content/pm/UserInfo.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Per-user information. + * @hide + */ +public class UserInfo implements Parcelable { + /** + * Primary user. Only one user can have this flag set. Meaning of this + * flag TBD. + */ + public static final int FLAG_PRIMARY = 0x00000001; + + /** + * User with administrative privileges. Such a user can create and + * delete users. + */ + public static final int FLAG_ADMIN = 0x00000002; + + /** + * Indicates a guest user that may be transient. + */ + public static final int FLAG_GUEST = 0x00000004; + + public int id; + public String name; + public int flags; + + public UserInfo(int id, String name, int flags) { + this.id = id; + this.name = name; + this.flags = flags; + } + + public boolean isPrimary() { + return (flags & FLAG_PRIMARY) == FLAG_PRIMARY; + } + + public boolean isAdmin() { + return (flags & FLAG_ADMIN) == FLAG_ADMIN; + } + + public boolean isGuest() { + return (flags & FLAG_GUEST) == FLAG_GUEST; + } + + public UserInfo() { + } + + public UserInfo(UserInfo orig) { + name = orig.name; + id = orig.id; + flags = orig.flags; + } + + @Override + public String toString() { + return "UserInfo{" + id + ":" + name + ":" + Integer.toHexString(flags) + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + dest.writeInt(id); + dest.writeString(name); + dest.writeInt(flags); + } + + public static final Parcelable.Creator<UserInfo> CREATOR + = new Parcelable.Creator<UserInfo>() { + public UserInfo createFromParcel(Parcel source) { + return new UserInfo(source); + } + public UserInfo[] newArray(int size) { + return new UserInfo[size]; + } + }; + + private UserInfo(Parcel source) { + id = source.readInt(); + name = source.readString(); + flags = source.readInt(); + } +} diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index dbb4271..931cb18 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -17,7 +17,6 @@ package android.content.res; import android.os.ParcelFileDescriptor; -import android.util.Config; import android.util.Log; import android.util.TypedValue; @@ -58,7 +57,7 @@ public final class AssetManager { public static final int ACCESS_BUFFER = 3; private static final String TAG = "AssetManager"; - private static final boolean localLOGV = Config.LOGV || false; + private static final boolean localLOGV = false || false; private static final boolean DEBUG_REFS = false; diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java index 23a6f97..63e33ce 100644 --- a/core/java/android/content/res/StringBlock.java +++ b/core/java/android/content/res/StringBlock.java @@ -18,7 +18,6 @@ package android.content.res; import android.text.*; import android.text.style.*; -import android.util.Config; import android.util.Log; import android.util.SparseArray; import android.graphics.Paint; @@ -34,7 +33,7 @@ import com.android.internal.util.XmlUtils; */ final class StringBlock { private static final String TAG = "AssetManager"; - private static final boolean localLOGV = Config.LOGV || false; + private static final boolean localLOGV = false || false; private final int mNative; private final boolean mUseSparse; diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java index 3ffc714..b6487bd 100644 --- a/core/java/android/database/AbstractCursor.java +++ b/core/java/android/database/AbstractCursor.java @@ -19,7 +19,6 @@ package android.database; import android.content.ContentResolver; import android.net.Uri; import android.os.Bundle; -import android.util.Config; import android.util.Log; import java.lang.ref.WeakReference; @@ -285,7 +284,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { } } - if (Config.LOGV) { + if (false) { if (getCount() > 0) { Log.w("AbstractCursor", "Unknown column " + columnName); } diff --git a/core/java/android/database/CursorToBulkCursorAdaptor.java b/core/java/android/database/CursorToBulkCursorAdaptor.java index 8bc7de2..8fa4d3b 100644 --- a/core/java/android/database/CursorToBulkCursorAdaptor.java +++ b/core/java/android/database/CursorToBulkCursorAdaptor.java @@ -19,7 +19,6 @@ package android.database; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; -import android.util.Config; import android.util.Log; @@ -77,7 +76,7 @@ public final class CursorToBulkCursorAdaptor extends BulkCursorNative if (mCursor instanceof AbstractWindowedCursor) { AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor; if (windowedCursor.hasWindow()) { - if (Log.isLoggable(TAG, Log.VERBOSE) || Config.LOGV) { + if (Log.isLoggable(TAG, Log.VERBOSE) || false) { Log.v(TAG, "Cross process cursor has a local window before setWindow in " + providerName, new RuntimeException()); } diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index f428aad..8e6f699 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -33,7 +33,6 @@ import android.database.sqlite.SQLiteStatement; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.text.TextUtils; -import android.util.Config; import android.util.Log; import java.io.FileNotFoundException; @@ -49,7 +48,7 @@ public class DatabaseUtils { private static final String TAG = "DatabaseUtils"; private static final boolean DEBUG = false; - private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean LOCAL_LOGV = false; private static final String[] countProjection = new String[]{"count(*)"}; diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index 4c2d123..ea9346d 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -23,7 +23,6 @@ import android.os.Handler; import android.os.Message; import android.os.Process; import android.os.StrictMode; -import android.util.Config; import android.util.Log; import java.util.HashMap; @@ -241,7 +240,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { mColumnNameMap = null; mQuery = query; - query.mDatabase.lock(); + query.mDatabase.lock(query.mSql); try { // Setup the list of columns int columnCount = mQuery.columnCountLocked(); @@ -251,7 +250,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { for (int i = 0; i < columnCount; i++) { String columnName = mQuery.columnNameLocked(i); mColumns[i] = columnName; - if (Config.LOGV) { + if (false) { Log.v("DatabaseWindow", "mColumns[" + i + "] is " + mColumns[i]); } @@ -366,13 +365,13 @@ public class SQLiteCursor extends AbstractWindowedCursor { } private void deactivateCommon() { - if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); + if (false) Log.v(TAG, "<<< Releasing cursor " + this); mCursorState = 0; if (mWindow != null) { mWindow.close(); mWindow = null; } - if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()"); + if (false) Log.v("DatabaseWindow", "closing window in release()"); } @Override @@ -398,7 +397,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { return false; } long timeStart = 0; - if (Config.LOGV) { + if (false) { timeStart = System.currentTimeMillis(); } @@ -419,7 +418,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { // since we need to use a different database connection handle, // re-compile the query try { - db.lock(); + db.lock(mQuery.mSql); } catch (IllegalStateException e) { // for backwards compatibility, just return false Log.w(TAG, "requery() failed " + e.getMessage(), e); @@ -453,7 +452,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { } } - if (Config.LOGV) { + if (false) { Log.v("DatabaseWindow", "closing window in requery()"); Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); } @@ -465,7 +464,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { // for backwards compatibility, just return false Log.w(TAG, "requery() failed " + e.getMessage(), e); } - if (Config.LOGV) { + if (false) { long timeEnd = System.currentTimeMillis(); Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); } @@ -513,7 +512,7 @@ public class SQLiteCursor extends AbstractWindowedCursor { close(); SQLiteDebug.notifyActiveCursorFinalized(); } else { - if (Config.LOGV) { + if (false) { Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + ", query = " + mQuery.mSql); } diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 90a5b5d..93a6ad3 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -30,7 +30,6 @@ import android.os.StatFs; import android.os.SystemClock; import android.os.SystemProperties; import android.text.TextUtils; -import android.util.Config; import android.util.EventLog; import android.util.Log; import android.util.LruCache; @@ -230,9 +229,23 @@ public class SQLiteDatabase extends SQLiteClosable { private static int sQueryLogTimeInMillis = 0; // lazily initialized private static final int QUERY_LOG_SQL_LENGTH = 64; private static final String COMMIT_SQL = "COMMIT;"; + private static final String BEGIN_SQL = "BEGIN;"; private final Random mRandom = new Random(); + /** the last non-commit/rollback sql statement in a transaction */ + // guarded by 'this' private String mLastSqlStatement = null; + synchronized String getLastSqlStatement() { + return mLastSqlStatement; + } + + synchronized void setLastSqlStatement(String sql) { + mLastSqlStatement = sql; + } + + /** guarded by {@link #mLock} */ + private long mTransStartTime; + // String prefix for slow database query EventLog records that show // lock acquistions of the database. /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:"; @@ -386,11 +399,16 @@ public class SQLiteDatabase extends SQLiteClosable { * * @see #unlock() */ - /* package */ void lock() { - lock(false); + /* package */ void lock(String sql) { + lock(sql, false); + } + + /* pachage */ void lock() { + lock(null, false); } + private static final long LOCK_WAIT_PERIOD = 30L; - private void lock(boolean forced) { + private void lock(String sql, boolean forced) { // make sure this method is NOT being called from a 'synchronized' method if (Thread.holdsLock(this)) { Log.w(TAG, "don't lock() while in a synchronized method"); @@ -398,6 +416,7 @@ public class SQLiteDatabase extends SQLiteClosable { verifyDbIsOpen(); if (!forced && !mLockingEnabled) return; boolean done = false; + long timeStart = SystemClock.uptimeMillis(); while (!done) { try { // wait for 30sec to acquire the lock @@ -420,6 +439,9 @@ public class SQLiteDatabase extends SQLiteClosable { mLockAcquiredThreadTime = Debug.threadCpuTimeNanos(); } } + if (sql != null) { + logTimeStat(sql, timeStart, GET_LOCK_LOG_PREFIX); + } } private static class DatabaseReentrantLock extends ReentrantLock { DatabaseReentrantLock(boolean fair) { @@ -444,7 +466,11 @@ public class SQLiteDatabase extends SQLiteClosable { * @see #unlockForced() */ private void lockForced() { - lock(true); + lock(null, true); + } + + private void lockForced(String sql) { + lock(sql, true); } /** @@ -612,7 +638,7 @@ public class SQLiteDatabase extends SQLiteClosable { private void beginTransaction(SQLiteTransactionListener transactionListener, boolean exclusive) { verifyDbIsOpen(); - lockForced(); + lockForced(BEGIN_SQL); boolean ok = false; try { // If this thread already had the lock then get out @@ -635,6 +661,7 @@ public class SQLiteDatabase extends SQLiteClosable { } else { execSQL("BEGIN IMMEDIATE;"); } + mTransStartTime = SystemClock.uptimeMillis(); mTransactionListener = transactionListener; mTransactionIsSuccessful = true; mInnerTransactionIsSuccessful = false; @@ -698,6 +725,8 @@ public class SQLiteDatabase extends SQLiteClosable { Log.i(TAG, "PRAGMA wal_Checkpoint done"); } } + // log the transaction time to the Eventlog. + logTimeStat(getLastSqlStatement(), mTransStartTime, COMMIT_SQL); } else { try { execSQL("ROLLBACK;"); @@ -705,7 +734,7 @@ public class SQLiteDatabase extends SQLiteClosable { throw savedException; } } catch (SQLException e) { - if (Config.LOGD) { + if (false) { Log.d(TAG, "exception during rollback, maybe the DB previously " + "performed an auto-rollback"); } @@ -714,7 +743,7 @@ public class SQLiteDatabase extends SQLiteClosable { } finally { mTransactionListener = null; unlockForced(); - if (Config.LOGV) { + if (false) { Log.v(TAG, "unlocked " + Thread.currentThread() + ", holdCount is " + mLock.getHoldCount()); } @@ -1527,7 +1556,7 @@ public class SQLiteDatabase extends SQLiteClosable { BlockGuard.getThreadPolicy().onReadFromDisk(); long timeStart = 0; - if (Config.LOGV || mSlowQueryThreshold != -1) { + if (false || mSlowQueryThreshold != -1) { timeStart = System.currentTimeMillis(); } @@ -1540,7 +1569,7 @@ public class SQLiteDatabase extends SQLiteClosable { cursorFactory != null ? cursorFactory : mFactory, selectionArgs); } finally { - if (Config.LOGV || mSlowQueryThreshold != -1) { + if (false || mSlowQueryThreshold != -1) { // Force query execution int count = -1; @@ -1550,7 +1579,7 @@ public class SQLiteDatabase extends SQLiteClosable { long duration = System.currentTimeMillis() - timeStart; - if (Config.LOGV || duration >= mSlowQueryThreshold) { + if (false || duration >= mSlowQueryThreshold) { Log.v(SQLiteCursor.TAG, "query (" + duration + " ms): " + driver.toString() + ", args are " + (selectionArgs != null @@ -1855,24 +1884,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @throws SQLException if the SQL string is invalid */ public void execSQL(String sql) throws SQLException { - int stmtType = DatabaseUtils.getSqlStatementType(sql); - if (stmtType == DatabaseUtils.STATEMENT_ATTACH) { - disableWriteAheadLogging(); - } - long timeStart = SystemClock.uptimeMillis(); - logTimeStat(mLastSqlStatement, timeStart, GET_LOCK_LOG_PREFIX); executeSql(sql, null); - - if (stmtType == DatabaseUtils.STATEMENT_ATTACH) { - mHasAttachedDbs = true; - } - // Log commit statements along with the most recently executed - // SQL statement for disambiguation. - if (stmtType == DatabaseUtils.STATEMENT_COMMIT) { - logTimeStat(mLastSqlStatement, timeStart, COMMIT_SQL); - } else { - logTimeStat(sql, timeStart, null); - } } /** @@ -1926,19 +1938,19 @@ public class SQLiteDatabase extends SQLiteClosable { } private int executeSql(String sql, Object[] bindArgs) throws SQLException { - long timeStart = SystemClock.uptimeMillis(); - int n; + if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) { + disableWriteAheadLogging(); + mHasAttachedDbs = true; + } SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs); try { - n = statement.executeUpdateDelete(); + return statement.executeUpdateDelete(); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); throw e; } finally { statement.close(); } - logTimeStat(sql, timeStart); - return n; } @Override @@ -2027,12 +2039,7 @@ public class SQLiteDatabase extends SQLiteClosable { logTimeStat(sql, beginMillis, null); } - /* package */ void logTimeStat(String sql, long beginMillis, String prefix) { - // Keep track of the last statement executed here, as this is - // the common funnel through which all methods of hitting - // libsqlite eventually flow. - mLastSqlStatement = sql; - + private void logTimeStat(String sql, long beginMillis, String prefix) { // Sample fast queries in proportion to the time taken. // Quantize the % first, so the logged sampling probability // exactly equals the actual sampling rate for this query. @@ -2059,7 +2066,6 @@ public class SQLiteDatabase extends SQLiteClosable { if (prefix != null) { sql = prefix + sql; } - if (sql.length() > QUERY_LOG_SQL_LENGTH) sql = sql.substring(0, QUERY_LOG_SQL_LENGTH); // ActivityThread.currentPackageName() only returns non-null if the diff --git a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java index de2fca9..a5e762e 100644 --- a/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java +++ b/core/java/android/database/sqlite/SQLiteDirectCursorDriver.java @@ -42,7 +42,7 @@ public class SQLiteDirectCursorDriver implements SQLiteCursorDriver { SQLiteQuery query = null; try { - mDatabase.lock(); + mDatabase.lock(mSql); mDatabase.closePendingStatements(); query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs); diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index 88246e8..89552dc 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -105,12 +105,9 @@ public abstract class SQLiteProgram extends SQLiteClosable { case DatabaseUtils.STATEMENT_SELECT: mStatementType = n | STATEMENT_CACHEABLE | STATEMENT_USE_POOLED_CONN; break; - case DatabaseUtils.STATEMENT_ATTACH: case DatabaseUtils.STATEMENT_BEGIN: case DatabaseUtils.STATEMENT_COMMIT: case DatabaseUtils.STATEMENT_ABORT: - case DatabaseUtils.STATEMENT_DDL: - case DatabaseUtils.STATEMENT_UNPREPARED: mStatementType = n | STATEMENT_DONT_PREPARE; break; default: @@ -353,13 +350,10 @@ public abstract class SQLiteProgram extends SQLiteClosable { /* package */ void compileAndbindAllArgs() { if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) { - // no need to prepare this SQL statement - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - if (mBindArgs != null) { - throw new IllegalArgumentException("no need to pass bindargs for this sql :" + - mSql); - } + if (mBindArgs != null) { + throw new IllegalArgumentException("Can't pass bindargs for this sql :" + mSql); } + // no need to prepare this SQL statement return; } if (nStatement == 0) { diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index e9e0172..dc882d9 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -70,9 +70,8 @@ public class SQLiteQuery extends SQLiteProgram { */ /* package */ int fillWindow(CursorWindow window, int maxRead, int lastPos) { + mDatabase.lock(mSql); long timeStart = SystemClock.uptimeMillis(); - mDatabase.lock(); - mDatabase.logTimeStat(mSql, timeStart, SQLiteDatabase.GET_LOCK_LOG_PREFIX); try { acquireReference(); try { diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index c76cc6c..ff973a7 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -80,7 +80,8 @@ public class SQLiteStatement extends SQLiteProgram */ public int executeUpdateDelete() { try { - long timeStart = acquireAndLock(WRITE); + saveSqlAsLastSqlStatement(); + acquireAndLock(WRITE); int numChanges = 0; if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) { // since the statement doesn't have to be prepared, @@ -90,7 +91,6 @@ public class SQLiteStatement extends SQLiteProgram } else { numChanges = native_execute(); } - mDatabase.logTimeStat(mSql, timeStart); return numChanges; } finally { releaseAndUnlock(); @@ -108,15 +108,22 @@ public class SQLiteStatement extends SQLiteProgram */ public long executeInsert() { try { - long timeStart = acquireAndLock(WRITE); - long lastInsertedRowId = native_executeInsert(); - mDatabase.logTimeStat(mSql, timeStart); - return lastInsertedRowId; + saveSqlAsLastSqlStatement(); + acquireAndLock(WRITE); + return native_executeInsert(); } finally { releaseAndUnlock(); } } + private void saveSqlAsLastSqlStatement() { + if (((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == + DatabaseUtils.STATEMENT_UPDATE) || + (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == + DatabaseUtils.STATEMENT_BEGIN) { + mDatabase.setLastSqlStatement(mSql); + } + } /** * Execute a statement that returns a 1 by 1 table with a numeric value. * For example, SELECT COUNT(*) FROM table; @@ -199,7 +206,7 @@ public class SQLiteStatement extends SQLiteProgram * <li>if the SQL statement is an update, start transaction if not already in one. * otherwise, get lock on the database</li> * <li>acquire reference on this object</li> - * <li>and then return the current time _before_ the database lock was acquired</li> + * <li>and then return the current time _after_ the database lock was acquired</li> * </ul> * <p> * This method removes the duplicate code from the other public @@ -243,7 +250,7 @@ public class SQLiteStatement extends SQLiteProgram } // do I have database lock? if not, grab it. if (!mDatabase.isDbLockedByCurrentThread()) { - mDatabase.lock(); + mDatabase.lock(mSql); mState = LOCK_ACQUIRED; } diff --git a/core/java/android/ddm/DdmHandleAppName.java b/core/java/android/ddm/DdmHandleAppName.java index 4a57d12..78dd23e 100644 --- a/core/java/android/ddm/DdmHandleAppName.java +++ b/core/java/android/ddm/DdmHandleAppName.java @@ -19,7 +19,6 @@ package android.ddm; import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; -import android.util.Config; import android.util.Log; import java.nio.ByteBuffer; @@ -88,7 +87,7 @@ public class DdmHandleAppName extends ChunkHandler { * Send an APNM (APplication NaMe) chunk. */ private static void sendAPNM(String appName) { - if (Config.LOGV) + if (false) Log.v("ddm", "Sending app name"); ByteBuffer out = ByteBuffer.allocate(4 + appName.length()*2); diff --git a/core/java/android/ddm/DdmHandleExit.java b/core/java/android/ddm/DdmHandleExit.java index 8a0b9a4..74ae37a 100644 --- a/core/java/android/ddm/DdmHandleExit.java +++ b/core/java/android/ddm/DdmHandleExit.java @@ -19,7 +19,6 @@ package android.ddm; import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; -import android.util.Config; import android.util.Log; import java.nio.ByteBuffer; @@ -59,7 +58,7 @@ public class DdmHandleExit extends ChunkHandler { * Handle a chunk of data. We're only registered for "EXIT". */ public Chunk handleChunk(Chunk request) { - if (Config.LOGV) + if (false) Log.v("ddm-exit", "Handling " + name(request.type) + " chunk"); /* diff --git a/core/java/android/ddm/DdmHandleHeap.java b/core/java/android/ddm/DdmHandleHeap.java index fa0fbbf..cece556 100644 --- a/core/java/android/ddm/DdmHandleHeap.java +++ b/core/java/android/ddm/DdmHandleHeap.java @@ -21,7 +21,6 @@ import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import org.apache.harmony.dalvik.ddmc.DdmVmInternal; import android.os.Debug; -import android.util.Config; import android.util.Log; import java.io.IOException; import java.nio.ByteBuffer; @@ -78,7 +77,7 @@ public class DdmHandleHeap extends ChunkHandler { * Handle a chunk of data. */ public Chunk handleChunk(Chunk request) { - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Handling " + name(request.type) + " chunk"); int type = request.type; @@ -113,7 +112,7 @@ public class DdmHandleHeap extends ChunkHandler { ByteBuffer in = wrapChunk(request); int when = in.get(); - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Heap segment enable: when=" + when); boolean ok = DdmVmInternal.heapInfoNotify(when); @@ -132,7 +131,7 @@ public class DdmHandleHeap extends ChunkHandler { int when = in.get(); int what = in.get(); - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Heap segment enable: when=" + when + ", what=" + what + ", isNative=" + isNative); @@ -160,7 +159,7 @@ public class DdmHandleHeap extends ChunkHandler { /* get the filename for the output file */ int len = in.getInt(); String fileName = getString(in, len); - if (Config.LOGD) + if (false) Log.d("ddm-heap", "Heap dump: file='" + fileName + "'"); try { @@ -192,7 +191,7 @@ public class DdmHandleHeap extends ChunkHandler { byte result; /* get the filename for the output file */ - if (Config.LOGD) + if (false) Log.d("ddm-heap", "Heap dump: [DDMS]"); String failMsg = null; @@ -218,7 +217,7 @@ public class DdmHandleHeap extends ChunkHandler { private Chunk handleHPGC(Chunk request) { //ByteBuffer in = wrapChunk(request); - if (Config.LOGD) + if (false) Log.d("ddm-heap", "Heap GC request"); System.gc(); @@ -234,7 +233,7 @@ public class DdmHandleHeap extends ChunkHandler { enable = (in.get() != 0); - if (Config.LOGD) + if (false) Log.d("ddm-heap", "Recent allocation enable request: " + enable); DdmVmInternal.enableRecentAllocations(enable); @@ -259,7 +258,7 @@ public class DdmHandleHeap extends ChunkHandler { private Chunk handleREAL(Chunk request) { //ByteBuffer in = wrapChunk(request); - if (Config.LOGD) + if (false) Log.d("ddm-heap", "Recent allocations request"); /* generate the reply in a ready-to-go format */ diff --git a/core/java/android/ddm/DdmHandleHello.java b/core/java/android/ddm/DdmHandleHello.java index 714a611..5088d22 100644 --- a/core/java/android/ddm/DdmHandleHello.java +++ b/core/java/android/ddm/DdmHandleHello.java @@ -19,7 +19,6 @@ package android.ddm; import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; -import android.util.Config; import android.util.Log; import android.os.Debug; @@ -53,7 +52,7 @@ public class DdmHandleHello extends ChunkHandler { * send messages to the server. */ public void connected() { - if (Config.LOGV) + if (false) Log.v("ddm-hello", "Connected!"); if (false) { @@ -70,7 +69,7 @@ public class DdmHandleHello extends ChunkHandler { * periodic transmissions or clean up saved state. */ public void disconnected() { - if (Config.LOGV) + if (false) Log.v("ddm-hello", "Disconnected!"); } @@ -78,7 +77,7 @@ public class DdmHandleHello extends ChunkHandler { * Handle a chunk of data. */ public Chunk handleChunk(Chunk request) { - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Handling " + name(request.type) + " chunk"); int type = request.type; @@ -105,7 +104,7 @@ public class DdmHandleHello extends ChunkHandler { ByteBuffer in = wrapChunk(request); int serverProtoVers = in.getInt(); - if (Config.LOGV) + if (false) Log.v("ddm-hello", "Server version is " + serverProtoVers); /* @@ -150,7 +149,7 @@ public class DdmHandleHello extends ChunkHandler { // is actually compiled in final String[] features = Debug.getVmFeatureList(); - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Got feature list request"); int size = 4 + 4 * features.length; diff --git a/core/java/android/ddm/DdmHandleProfiling.java b/core/java/android/ddm/DdmHandleProfiling.java index 63ee445..e0db5e7 100644 --- a/core/java/android/ddm/DdmHandleProfiling.java +++ b/core/java/android/ddm/DdmHandleProfiling.java @@ -20,7 +20,6 @@ import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import android.os.Debug; -import android.util.Config; import android.util.Log; import java.io.IOException; import java.nio.ByteBuffer; @@ -69,7 +68,7 @@ public class DdmHandleProfiling extends ChunkHandler { * Handle a chunk of data. */ public Chunk handleChunk(Chunk request) { - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Handling " + name(request.type) + " chunk"); int type = request.type; @@ -99,7 +98,7 @@ public class DdmHandleProfiling extends ChunkHandler { int flags = in.getInt(); int len = in.getInt(); String fileName = getString(in, len); - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Method profiling start: filename='" + fileName + "', size=" + bufferSize + ", flags=" + flags); @@ -139,7 +138,7 @@ public class DdmHandleProfiling extends ChunkHandler { int bufferSize = in.getInt(); int flags = in.getInt(); - if (Config.LOGV) { + if (false) { Log.v("ddm-heap", "Method prof stream start: size=" + bufferSize + ", flags=" + flags); } @@ -158,7 +157,7 @@ public class DdmHandleProfiling extends ChunkHandler { private Chunk handleMPSE(Chunk request) { byte result; - if (Config.LOGV) { + if (false) { Log.v("ddm-heap", "Method prof stream end"); } diff --git a/core/java/android/ddm/DdmHandleThread.java b/core/java/android/ddm/DdmHandleThread.java index c307988..613ab75 100644 --- a/core/java/android/ddm/DdmHandleThread.java +++ b/core/java/android/ddm/DdmHandleThread.java @@ -20,7 +20,6 @@ import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import org.apache.harmony.dalvik.ddmc.DdmVmInternal; -import android.util.Config; import android.util.Log; import java.nio.ByteBuffer; @@ -66,7 +65,7 @@ public class DdmHandleThread extends ChunkHandler { * Handle a chunk of data. */ public Chunk handleChunk(Chunk request) { - if (Config.LOGV) + if (false) Log.v("ddm-thread", "Handling " + name(request.type) + " chunk"); int type = request.type; diff --git a/core/java/android/ddm/DdmRegister.java b/core/java/android/ddm/DdmRegister.java index debf189..ecd450d 100644 --- a/core/java/android/ddm/DdmRegister.java +++ b/core/java/android/ddm/DdmRegister.java @@ -17,7 +17,6 @@ package android.ddm; import org.apache.harmony.dalvik.ddmc.DdmServer; -import android.util.Config; import android.util.Log; /** @@ -44,7 +43,7 @@ public class DdmRegister { * we finish here. */ public static void registerHandlers() { - if (Config.LOGV) + if (false) Log.v("ddm", "Registering DDM message handlers"); DdmHandleHello.register(); DdmHandleThread.register(); diff --git a/core/java/android/gesture/Gesture.java b/core/java/android/gesture/Gesture.java index 300cd28..c6a2a87 100755 --- a/core/java/android/gesture/Gesture.java +++ b/core/java/android/gesture/Gesture.java @@ -36,7 +36,7 @@ import java.util.concurrent.atomic.AtomicInteger; /** * A gesture is a hand-drawn shape on a touch screen. It can have one or multiple strokes. * Each stroke is a sequence of timed points. A user-defined gesture can be recognized by - * a GestureLibrary and a built-in alphabet gesture can be recognized by a LetterRecognizer. + * a GestureLibrary. */ public class Gesture implements Parcelable { diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 97f0e1b..d5c4ace 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -27,6 +27,7 @@ import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; import android.graphics.ImageFormat; +import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.os.Handler; import android.os.Looper; @@ -374,6 +375,12 @@ public class Camera { * The preview surface texture may not otherwise change while preview is * running. * + * The timestamps provided by {@link SurfaceTexture#getTimestamp()} for a + * SurfaceTexture set as the preview texture have an unspecified zero point, + * and cannot be directly compared between different cameras or different + * instances of the same camera, or across multiple runs of the same + * program. + * * @param surfaceTexture the {@link SurfaceTexture} to which the preview * images are to be sent or null to remove the current preview surface * texture @@ -410,8 +417,9 @@ public class Camera { /** * Starts capturing and drawing preview frames to the screen. - * Preview will not actually start until a surface is supplied with - * {@link #setPreviewDisplay(SurfaceHolder)}. + * Preview will not actually start until a surface is supplied + * with {@link #setPreviewDisplay(SurfaceHolder)} or + * {@link #setPreviewTexture(SurfaceTexture)}. * * <p>If {@link #setPreviewCallback(Camera.PreviewCallback)}, * {@link #setOneShotPreviewCallback(Camera.PreviewCallback)}, or @@ -1076,6 +1084,51 @@ public class Camera { }; /** + * Area class for focus. + * + * @see #setFocusAreas(List) + * @see #getFocusAreas() + */ + public static class Area { + /** + * Create an area with specified rectangle and weight. + * + * @param rect the rectangle of the area + * @param weight the weight of the area + */ + public Area(Rect rect, int weight) { + this.rect = rect; + this.weight = weight; + } + /** + * Compares {@code obj} to this area. + * + * @param obj the object to compare this area with. + * @return {@code true} if the rectangle and weight of {@code obj} is + * the same as those of this area. {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Area)) { + return false; + } + Area a = (Area) obj; + if (rect == null) { + if (a.rect != null) return false; + } else { + if (!rect.equals(a.rect)) return false; + } + return weight == a.weight; + } + + /** rectangle of the area */ + public Rect rect; + + /** weight of the area */ + public int weight; + }; + + /** * Camera service settings. * * <p>To make camera parameters take effect, applications have to call @@ -1117,6 +1170,8 @@ public class Camera { private static final String KEY_SCENE_MODE = "scene-mode"; private static final String KEY_FLASH_MODE = "flash-mode"; private static final String KEY_FOCUS_MODE = "focus-mode"; + private static final String KEY_FOCUS_AREAS = "focus-areas"; + private static final String KEY_MAX_NUM_FOCUS_AREAS = "max-num-focus-areas"; private static final String KEY_FOCAL_LENGTH = "focal-length"; private static final String KEY_HORIZONTAL_VIEW_ANGLE = "horizontal-view-angle"; private static final String KEY_VERTICAL_VIEW_ANGLE = "vertical-view-angle"; @@ -1124,6 +1179,12 @@ public class Camera { private static final String KEY_MAX_EXPOSURE_COMPENSATION = "max-exposure-compensation"; private static final String KEY_MIN_EXPOSURE_COMPENSATION = "min-exposure-compensation"; private static final String KEY_EXPOSURE_COMPENSATION_STEP = "exposure-compensation-step"; + private static final String KEY_AUTO_EXPOSURE_LOCK = "auto-exposure-lock"; + private static final String KEY_AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported"; + private static final String KEY_AUTO_WHITEBALANCE_LOCK = "auto-whitebalance-lock"; + private static final String KEY_AUTO_WHITEBALANCE_LOCK_SUPPORTED = "auto-whitebalance-lock-supported"; + private static final String KEY_METERING_AREAS = "metering-areas"; + private static final String KEY_MAX_NUM_METERING_AREAS = "max-num-metering-areas"; private static final String KEY_ZOOM = "zoom"; private static final String KEY_MAX_ZOOM = "max-zoom"; private static final String KEY_ZOOM_RATIOS = "zoom-ratios"; @@ -1138,6 +1199,7 @@ public class Camera { private static final String SUPPORTED_VALUES_SUFFIX = "-values"; private static final String TRUE = "true"; + private static final String FALSE = "false"; // Values for white balance settings. public static final String WHITE_BALANCE_AUTO = "auto"; @@ -1462,6 +1524,31 @@ public class Camera { mMap.put(key, Integer.toString(value)); } + private void set(String key, List<Area> areas) { + if (areas == null) { + set(key, "(0,0,0,0,0)"); + } else { + StringBuilder buffer = new StringBuilder(); + for (int i = 0; i < areas.size(); i++) { + Area area = areas.get(i); + Rect rect = area.rect; + buffer.append('('); + buffer.append(rect.left); + buffer.append(','); + buffer.append(rect.top); + buffer.append(','); + buffer.append(rect.right); + buffer.append(','); + buffer.append(rect.bottom); + buffer.append(','); + buffer.append(area.weight); + buffer.append(')'); + if (i != areas.size() - 1) buffer.append(','); + } + set(key, buffer.toString()); + } + } + /** * Returns the value of a String parameter. * @@ -1483,7 +1570,9 @@ public class Camera { } /** - * Sets the dimensions for preview pictures. + * Sets the dimensions for preview pictures. If the preview has already + * started, applications should stop the preview first before changing + * preview size. * * The sides of width and height are based on camera orientation. That * is, the preview size is the size before it is rotated by display @@ -2381,6 +2470,175 @@ public class Camera { } /** + * <p>Sets the auto-exposure lock state. Applications should check + * {@link #isAutoExposureLockSupported} before using this method.</p> + * + * <p>If set to true, the camera auto-exposure routine will immediately + * pause until the lock is set to false. Exposure compensation settings + * changes will still take effect while auto-exposure is locked.</p> + * + * <p>If auto-exposure is already locked, setting this to true again has + * no effect (the driver will not recalculate exposure values).</p> + * + * <p>Stopping preview with {@link #stopPreview()}, or triggering still + * image capture with {@link #takePicture(Camera.ShutterCallback, + * Camera.PictureCallback, Camera.PictureCallback)}, will automatically + * set the lock to false. However, the lock can be re-enabled before + * preview is re-started to keep the same AE parameters.</p> + * + * <p>Exposure compensation, in conjunction with re-enabling the AE and + * AWB locks after each still capture, can be used to capture an + * exposure-bracketed burst of images, for example.</p> + * + * <p>Auto-exposure state, including the lock state, will not be + * maintained after camera {@link #release()} is called. Locking + * auto-exposure after {@link #open()} but before the first call to + * {@link #startPreview()} will not allow the auto-exposure routine to + * run at all, and may result in severely over- or under-exposed + * images.</p> + * + * <p>The driver may also independently lock auto-exposure after + * auto-focus completes. If this is undesirable, be sure to always set + * the auto-exposure lock to false after the + * {@link AutoFocusCallback#onAutoFocus(boolean, Camera)} callback is + * received. The {@link #getAutoExposureLock()} method can be used after + * the callback to determine if the camera has locked auto-exposure + * independently.</p> + * + * @param toggle new state of the auto-exposure lock. True means that + * auto-exposure is locked, false means that the auto-exposure + * routine is free to run normally. + * + * @see #getAutoExposureLock() + * + * @hide + */ + public void setAutoExposureLock(boolean toggle) { + set(KEY_AUTO_EXPOSURE_LOCK, toggle ? TRUE : FALSE); + } + + /** + * Gets the state of the auto-exposure lock. Applications should check + * {@link #isAutoExposureLockSupported} before using this method. See + * {@link #setAutoExposureLock} for details about the lock. + * + * @return State of the auto-exposure lock. Returns true if + * auto-exposure is currently locked, and false otherwise. The + * auto-exposure lock may be independently enabled by the camera + * subsystem when auto-focus has completed. This method can be + * used after the {@link AutoFocusCallback#onAutoFocus(boolean, + * Camera)} callback to determine if the camera has locked AE. + * + * @see #setAutoExposureLock(boolean) + * + * @hide + */ + public boolean getAutoExposureLock() { + String str = get(KEY_AUTO_EXPOSURE_LOCK); + return TRUE.equals(str); + } + + /** + * Returns true if auto-exposure locking is supported. Applications + * should call this before trying to lock auto-exposure. See + * {@link #setAutoExposureLock} for details about the lock. + * + * @return true if auto-exposure lock is supported. + * @see #setAutoExposureLock(boolean) + * + * @hide + */ + public boolean isAutoExposureLockSupported() { + String str = get(KEY_AUTO_EXPOSURE_LOCK_SUPPORTED); + return TRUE.equals(str); + } + + /** + * <p>Sets the auto-white balance lock state. Applications should check + * {@link #isAutoWhiteBalanceLockSupported} before using this + * method.</p> + * + * <p>If set to true, the camera auto-white balance routine will + * immediately pause until the lock is set to false.</p> + * + * <p>If auto-white balance is already locked, setting this to true + * again has no effect (the driver will not recalculate white balance + * values).</p> + * + * <p>Stopping preview with {@link #stopPreview()}, or triggering still + * image capture with {@link #takePicture(Camera.ShutterCallback, + * Camera.PictureCallback, Camera.PictureCallback)}, will automatically + * set the lock to false. However, the lock can be re-enabled before + * preview is re-started to keep the same white balance parameters.</p> + * + * <p>Exposure compensation, in conjunction with re-enabling the AE and + * AWB locks after each still capture, can be used to capture an + * exposure-bracketed burst of images, for example. Auto-white balance + * state, including the lock state, will not be maintained after camera + * {@link #release()} is called. Locking auto-white balance after + * {@link #open()} but before the first call to {@link #startPreview()} + * will not allow the auto-white balance routine to run at all, and may + * result in severely incorrect color in captured images.</p> + * + * <p>The driver may also independently lock auto-white balance after + * auto-focus completes. If this is undesirable, be sure to always set + * the auto-white balance lock to false after the + * {@link AutoFocusCallback#onAutoFocus(boolean, Camera)} callback is + * received. The {@link #getAutoWhiteBalanceLock()} method can be used + * after the callback to determine if the camera has locked auto-white + * balance independently.</p> + * + * @param toggle new state of the auto-white balance lock. True means + * that auto-white balance is locked, false means that the + * auto-white balance routine is free to run normally. + * + * @see #getAutoWhiteBalanceLock() + * + * @hide + */ + public void setAutoWhiteBalanceLock(boolean toggle) { + set(KEY_AUTO_WHITEBALANCE_LOCK, toggle ? TRUE : FALSE); + } + + /** + * Gets the state of the auto-white balance lock. Applications should + * check {@link #isAutoWhiteBalanceLockSupported} before using this + * method. See {@link #setAutoWhiteBalanceLock} for details about the + * lock. + * + * @return State of the auto-white balance lock. Returns true if + * auto-white balance is currently locked, and false + * otherwise. The auto-white balance lock may be independently + * enabled by the camera subsystem when auto-focus has + * completed. This method can be used after the + * {@link AutoFocusCallback#onAutoFocus(boolean, Camera)} + * callback to determine if the camera has locked AWB. + * + * @see #setAutoWhiteBalanceLock(boolean) + * + * @hide + */ + public boolean getAutoWhiteBalanceLock() { + String str = get(KEY_AUTO_WHITEBALANCE_LOCK); + return TRUE.equals(str); + } + + /** + * Returns true if auto-white balance locking is supported. Applications + * should call this before trying to lock auto-white balance. See + * {@link #setAutoWhiteBalanceLock} for details about the lock. + * + * @return true if auto-white balance lock is supported. + * @see #setAutoWhiteBalanceLock(boolean) + * + * @hide + */ + public boolean isAutoWhiteBalanceLockSupported() { + String str = get(KEY_AUTO_WHITEBALANCE_LOCK_SUPPORTED); + return TRUE.equals(str); + } + + /** * Gets current zoom value. This also works when smooth zoom is in * progress. Applications should check {@link #isZoomSupported} before * using this method. @@ -2492,6 +2750,138 @@ public class Camera { splitFloat(get(KEY_FOCUS_DISTANCES), output); } + /** + * Gets the maximum number of focus areas supported. This is the maximum + * length of the list in {@link #setFocusAreas(List)} and + * {@link #getFocusAreas()}. + * + * @return the maximum number of focus areas supported by the camera. + * @see #getFocusAreas() + */ + public int getMaxNumFocusAreas() { + return getInt(KEY_MAX_NUM_FOCUS_AREAS, 0); + } + + /** + * Gets the current focus areas. Camera driver uses the areas to decide + * focus. + * + * Before using this API or {@link #setFocusAreas(List)}, apps should + * call {@link #getMaxNumFocusAreas()} to know the maximum number of + * focus areas first. If the value is 0, focus area is not supported. + * + * Each focus area is a rectangle with specified weight. The direction + * is relative to the sensor orientation, that is, what the sensor sees. + * The direction is not affected by the rotation or mirroring of + * {@link #setDisplayOrientation(int)}. Coordinates of the rectangle + * range from -1000 to 1000. (-1000, -1000) is the upper left point. + * (1000, 1000) is the lower right point. The length and width of focus + * areas cannot be 0 or negative. + * + * The weight must range from 1 to 1000. The weight should be + * interpreted as a per-pixel weight - all pixels in the area have the + * specified weight. This means a small area with the same weight as a + * larger area will have less influence on the focusing than the larger + * area. Focus areas can partially overlap and the driver will add the + * weights in the overlap region. + * + * A special case of null focus area means driver to decide the focus + * area. For example, the driver may use more signals to decide focus + * areas and change them dynamically. Apps can set all-zero if they want + * the driver to decide focus areas. + * + * Focus areas are relative to the current field of view + * ({@link #getZoom()}). No matter what the zoom level is, (-1000,-1000) + * represents the top of the currently visible camera frame. The focus + * area cannot be set to be outside the current field of view, even + * when using zoom. + * + * Focus area only has effect if the current focus mode is + * {@link #FOCUS_MODE_AUTO}, {@link #FOCUS_MODE_MACRO}, or + * {@link #FOCUS_MODE_CONTINUOUS_VIDEO}. + * + * @return a list of current focus areas + */ + public List<Area> getFocusAreas() { + return splitArea(get(KEY_FOCUS_AREAS)); + } + + /** + * Sets focus areas. See {@link #getFocusAreas()} for documentation. + * + * @param focusAreas the focus areas + * @see #getFocusAreas() + */ + public void setFocusAreas(List<Area> focusAreas) { + set(KEY_FOCUS_AREAS, focusAreas); + } + + /** + * Gets the maximum number of metering areas supported. This is the + * maximum length of the list in {@link #setMeteringAreas(List)} and + * {@link #getMeteringAreas()}. + * + * @return the maximum number of metering areas supported by the camera. + * @see #getMeteringAreas() + */ + public int getMaxNumMeteringAreas() { + return getInt(KEY_MAX_NUM_METERING_AREAS, 0); + } + + /** + * Gets the current metering areas. Camera driver uses these areas to + * decide exposure. + * + * Before using this API or {@link #setMeteringAreas(List)}, apps should + * call {@link #getMaxNumMeteringAreas()} to know the maximum number of + * metering areas first. If the value is 0, metering area is not + * supported. + * + * Each metering area is a rectangle with specified weight. The + * direction is relative to the sensor orientation, that is, what the + * sensor sees. The direction is not affected by the rotation or + * mirroring of {@link #setDisplayOrientation(int)}. Coordinates of the + * rectangle range from -1000 to 1000. (-1000, -1000) is the upper left + * point. (1000, 1000) is the lower right point. The length and width of + * metering areas cannot be 0 or negative. + * + * The weight must range from 1 to 1000, and represents a weight for + * every pixel in the area. This means that a large metering area with + * the same weight as a smaller area will have more effect in the + * metering result. Metering areas can partially overlap and the driver + * will add the weights in the overlap region. + * + * A special case of null metering area means driver to decide the + * metering area. For example, the driver may use more signals to decide + * metering areas and change them dynamically. Apps can set all-zero if + * they want the driver to decide metering areas. + * + * Metering areas are relative to the current field of view + * ({@link #getZoom()}). No matter what the zoom level is, (-1000,-1000) + * represents the top of the currently visible camera frame. The + * metering area cannot be set to be outside the current field of view, + * even when using zoom. + * + * No matter what metering areas are, the final exposure are compensated + * by {@link #setExposureCompensation(int)}. + * + * @return a list of current metering areas + */ + public List<Area> getMeteringAreas() { + return splitArea(KEY_METERING_AREAS); + } + + /** + * Sets metering areas. See {@link #getMeteringAreas()} for + * documentation. + * + * @param meteringAreas the metering areas + * @see #getMeteringAreas() + */ + public void setMeteringAreas(List<Area> meteringAreas) { + set(KEY_METERING_AREAS, meteringAreas); + } + // Splits a comma delimited string to an ArrayList of String. // Return null if the passing string is null or the size is 0. private ArrayList<String> split(String str) { @@ -2617,5 +3007,41 @@ public class Camera { if (rangeList.size() == 0) return null; return rangeList; } + + // Splits a comma delimited string to an ArrayList of Area objects. + // Example string: "(-10,-10,0,0,300),(0,0,10,10,700)". Return null if + // the passing string is null or the size is 0 or (0,0,0,0,0). + private ArrayList<Area> splitArea(String str) { + if (str == null || str.charAt(0) != '(' + || str.charAt(str.length() - 1) != ')') { + Log.e(TAG, "Invalid area string=" + str); + return null; + } + + ArrayList<Area> result = new ArrayList<Area>(); + int endIndex, fromIndex = 1; + int[] array = new int[5]; + do { + endIndex = str.indexOf("),(", fromIndex); + if (endIndex == -1) endIndex = str.length() - 1; + splitInt(str.substring(fromIndex, endIndex), array); + Rect rect = new Rect(array[0], array[1], array[2], array[3]); + result.add(new Area(rect, array[4])); + fromIndex = endIndex + 3; + } while (endIndex != str.length() - 1); + + if (result.size() == 0) return null; + + if (result.size() == 1) { + Area area = (Area) result.get(0); + Rect rect = area.rect; + if (rect.left == 0 && rect.top == 0 && rect.right == 0 + && rect.bottom == 0 && area.weight == 0) { + return null; + } + } + + return result; + } }; } diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java index 595c7d1..68fc101 100644 --- a/core/java/android/hardware/Sensor.java +++ b/core/java/android/hardware/Sensor.java @@ -66,7 +66,14 @@ public class Sensor { /** A constant describing a pressure sensor type */ public static final int TYPE_PRESSURE = 6; - /** A constant describing a temperature sensor type */ + /** + * A constant describing a temperature sensor type + * + * @deprecated use + * {@link android.hardware.Sensor#TYPE_AMBIENT_TEMPERATURE + * Sensor.TYPE_AMBIENT_TEMPERATURE} instead. + */ + @Deprecated public static final int TYPE_TEMPERATURE = 7; /** @@ -104,6 +111,9 @@ public class Sensor { */ public static final int TYPE_RELATIVE_HUMIDITY = 12; + /** A constant describing an ambient temperature sensor type */ + public static final int TYPE_AMBIENT_TEMPERATURE = 13; + /** * A constant describing all sensor types. */ diff --git a/core/java/android/hardware/SensorEvent.java b/core/java/android/hardware/SensorEvent.java index 9b62a68..0411b5c 100644 --- a/core/java/android/hardware/SensorEvent.java +++ b/core/java/android/hardware/SensorEvent.java @@ -204,6 +204,12 @@ public class SensorEvent { * values[0]: Ambient light level in SI lux units * </ul> * + * <h4>{@link android.hardware.Sensor#TYPE_PRESSURE Sensor.TYPE_PRESSURE}:</h4> + * <ul> + * <p> + * values[0]: Atmospheric pressure in hPa (millibar) + * </ul> + * * <h4>{@link android.hardware.Sensor#TYPE_PROXIMITY Sensor.TYPE_PROXIMITY}: * </h4> * @@ -247,6 +253,23 @@ public class SensorEvent { * <p>Elements of the rotation vector are unitless. * The x,y, and z axis are defined in the same way as the acceleration * sensor.</p> + * The reference coordinate system is defined as a direct orthonormal basis, + * where: + * </p> + * + * <ul> + * <li>X is defined as the vector product <b>Y.Z</b> (It is tangential to + * the ground at the device's current location and roughly points East).</li> + * <li>Y is tangential to the ground at the device's current location and + * points towards the magnetic North Pole.</li> + * <li>Z points towards the sky and is perpendicular to the ground.</li> + * </ul> + * + * <p> + * <center><img src="../../../images/axis_globe.png" + * alt="World coordinate-system diagram." border="0" /></center> + * </p> + * * <ul> * <p> * values[0]: x*sin(θ/2) @@ -362,6 +385,14 @@ public class SensorEvent { * dv = 216.7 * * (rh / 100.0 * 6.112 * Math.exp(17.62 * t / (243.12 + t)) / (273.15 + t)); * </pre> + * + * <h4>{@link android.hardware.Sensor#TYPE_AMBIENT_TEMPERATURE Sensor.TYPE_AMBIENT_TEMPERATURE}: + * </h4> + * + * <ul> + * <p> + * values[0]: ambient (room) temperature in degree Celsius. + * </ul> * * @see SensorEvent * @see GeomagneticField diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index ab5c78a..dfc70ef 100644 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -142,7 +142,8 @@ public class KeyboardView extends View implements View.OnClickListener { private int mPreviewTextSizeLarge; private int mPreviewOffset; private int mPreviewHeight; - private int[] mOffsetInWindow; + // Working variable + private final int[] mCoordinates = new int[2]; private PopupWindow mPopupKeyboard; private View mMiniKeyboardContainer; @@ -152,7 +153,6 @@ public class KeyboardView extends View implements View.OnClickListener { private int mMiniKeyboardOffsetX; private int mMiniKeyboardOffsetY; private Map<Key,View> mMiniKeyboardCache; - private int[] mWindowOffset; private Key[] mKeys; /** Listener for {@link OnKeyboardActionListener}. */ @@ -905,23 +905,19 @@ public class KeyboardView extends View implements View.OnClickListener { mPopupPreviewY = - mPreviewText.getMeasuredHeight(); } mHandler.removeMessages(MSG_REMOVE_PREVIEW); - if (mOffsetInWindow == null) { - mOffsetInWindow = new int[2]; - getLocationInWindow(mOffsetInWindow); - mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero - mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero - int[] mWindowLocation = new int[2]; - getLocationOnScreen(mWindowLocation); - mWindowY = mWindowLocation[1]; - } + getLocationInWindow(mCoordinates); + mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero + mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero + // Set the preview background state mPreviewText.getBackground().setState( key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); - mPopupPreviewX += mOffsetInWindow[0]; - mPopupPreviewY += mOffsetInWindow[1]; + mPopupPreviewX += mCoordinates[0]; + mPopupPreviewY += mCoordinates[1]; // If the popup cannot be shown above the key, put it on the side - if (mPopupPreviewY + mWindowY < 0) { + getLocationOnScreen(mCoordinates); + if (mPopupPreviewY + mCoordinates[1] < 0) { // If the key you're pressing is on the left side of the keyboard, show the popup on // the right, offset by enough to see at least one key to the left/right. if (key.x + key.width <= getWidth() / 2) { @@ -1057,16 +1053,13 @@ public class KeyboardView extends View implements View.OnClickListener { mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( com.android.internal.R.id.keyboardView); } - if (mWindowOffset == null) { - mWindowOffset = new int[2]; - getLocationInWindow(mWindowOffset); - } + getLocationInWindow(mCoordinates); mPopupX = popupKey.x + mPaddingLeft; mPopupY = popupKey.y + mPaddingTop; mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); - final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0]; - final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1]; + final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mCoordinates[0]; + final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mCoordinates[1]; mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); mMiniKeyboard.setShifted(isShifted()); mPopupKeyboard.setContentView(mMiniKeyboardContainer); diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java index 343242e..7159260 100644 --- a/core/java/android/inputmethodservice/SoftInputWindow.java +++ b/core/java/android/inputmethodservice/SoftInputWindow.java @@ -73,8 +73,17 @@ class SoftInputWindow extends Dialog { @Override public boolean dispatchTouchEvent(MotionEvent ev) { getWindow().getDecorView().getHitRect(mBounds); - final MotionEvent event = clipMotionEvent(ev, mBounds); - return super.dispatchTouchEvent(event); + + if (ev.isWithinBoundsNoHistory(mBounds.left, mBounds.top, + mBounds.right - 1, mBounds.bottom - 1)) { + return super.dispatchTouchEvent(ev); + } else { + MotionEvent temp = ev.clampNoHistory(mBounds.left, mBounds.top, + mBounds.right - 1, mBounds.bottom - 1); + boolean handled = super.dispatchTouchEvent(temp); + temp.recycle(); + return handled; + } } /** @@ -163,48 +172,4 @@ class SoftInputWindow extends Dialog { WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_DIM_BEHIND); } - - private static MotionEvent clipMotionEvent(MotionEvent me, Rect bounds) { - final int pointerCount = me.getPointerCount(); - boolean shouldClip = false; - for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) { - final int x = (int)me.getX(pointerIndex); - final int y = (int)me.getY(pointerIndex); - if (!bounds.contains(x, y)) { - shouldClip = true; - break; - } - } - if (!shouldClip) - return me; - - if (pointerCount == 1) { - final int x = (int)me.getX(); - final int y = (int)me.getY(); - me.setLocation( - Math.max(bounds.left, Math.min(x, bounds.right - 1)), - Math.max(bounds.top, Math.min(y, bounds.bottom - 1))); - return me; - } - - final int[] pointerIds = new int[pointerCount]; - final MotionEvent.PointerCoords[] pointerCoords = - new MotionEvent.PointerCoords[pointerCount]; - for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) { - pointerIds[pointerIndex] = me.getPointerId(pointerIndex); - final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); - me.getPointerCoords(pointerIndex, coords); - pointerCoords[pointerIndex] = coords; - final int x = (int)coords.x; - final int y = (int)coords.y; - if (!bounds.contains(x, y)) { - coords.x = Math.max(bounds.left, Math.min(x, bounds.right - 1)); - coords.y = Math.max(bounds.top, Math.min(y, bounds.bottom - 1)); - } - } - return MotionEvent.obtain( - me.getDownTime(), me.getEventTime(), me.getAction(), pointerCount, pointerIds, - pointerCoords, me.getMetaState(), me.getXPrecision(), me.getYPrecision(), - me.getDeviceId(), me.getEdgeFlags(), me.getSource(), me.getFlags()); - } } diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index b541ec3..419288b 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -215,15 +215,20 @@ public class ConnectivityManager /** * Bluetooth data connection. This is used for Bluetooth reverse tethering. - * @hide */ public static final int TYPE_BLUETOOTH = 7; - /** {@hide} */ + /** + * Dummy data connection. This should not be used on shipping devices. + */ public static final int TYPE_DUMMY = 8; - /** {@hide} */ + /** + * Ethernet data connection. This may be via USB dongle or more + * traditional means. + */ public static final int TYPE_ETHERNET = 9; + /** * Over the air Adminstration. * {@hide} diff --git a/core/java/android/net/INetworkPolicyManager.aidl b/core/java/android/net/INetworkPolicyManager.aidl new file mode 100644 index 0000000..fa6eae5 --- /dev/null +++ b/core/java/android/net/INetworkPolicyManager.aidl @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +/** + * Interface that creates and modifies network policy rules. + * + * {@hide} + */ +interface INetworkPolicyManager { + + void onForegroundActivitiesChanged(int uid, int pid, boolean foregroundActivities); + void onProcessDied(int uid, int pid); + + void setUidPolicy(int uid, int policy); + int getUidPolicy(int uid); + + // TODO: build API to surface stats details for settings UI + +} diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java new file mode 100644 index 0000000..2312bd9 --- /dev/null +++ b/core/java/android/net/NetworkPolicyManager.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.os.RemoteException; + +/** + * Manager for creating and modifying network policy rules. + * + * {@hide} + */ +public class NetworkPolicyManager { + + /** No specific network policy, use system default. */ + public static final int POLICY_NONE = 0x0; + /** Reject network usage when application in background. */ + public static final int POLICY_REJECT_BACKGROUND = 0x1; + /** Reject network usage on paid network connections. */ + public static final int POLICY_REJECT_PAID = 0x2; + /** Application should conserve data. */ + public static final int POLICY_CONSERVE_DATA = 0x4; + + private INetworkPolicyManager mService; + + public NetworkPolicyManager(INetworkPolicyManager service) { + if (service == null) { + throw new IllegalArgumentException("missing INetworkPolicyManager"); + } + mService = service; + } + + /** + * Set policy flags for specific UID. + * + * @param policy {@link #POLICY_NONE} or combination of + * {@link #POLICY_REJECT_BACKGROUND}, {@link #POLICY_REJECT_PAID}, + * or {@link #POLICY_CONSERVE_DATA}. + */ + public void setUidPolicy(int uid, int policy) { + try { + mService.setUidPolicy(uid, policy); + } catch (RemoteException e) { + } + } + + public int getUidPolicy(int uid) { + try { + return mService.getUidPolicy(uid); + } catch (RemoteException e) { + return POLICY_NONE; + } + } + +} diff --git a/core/java/android/net/NetworkStats.aidl b/core/java/android/net/NetworkStats.aidl new file mode 100644 index 0000000..d06ca65 --- /dev/null +++ b/core/java/android/net/NetworkStats.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2011, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +parcelable NetworkStats; diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java new file mode 100644 index 0000000..4430e00 --- /dev/null +++ b/core/java/android/net/NetworkStats.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; + +import java.io.CharArrayWriter; +import java.io.PrintWriter; + +/** + * Collection of network statistics. Can contain summary details across all + * interfaces, or details with per-UID granularity. Designed to parcel quickly + * across process boundaries. + * + * @hide + */ +public class NetworkStats implements Parcelable { + /** {@link #iface} value when entry is summarized over all interfaces. */ + public static final String IFACE_ALL = null; + /** {@link #uid} value when entry is summarized over all UIDs. */ + public static final int UID_ALL = 0; + + /** + * {@link SystemClock#elapsedRealtime()} timestamp when this data was + * generated. + */ + public final long elapsedRealtime; + public final String[] iface; + public final int[] uid; + public final long[] rx; + public final long[] tx; + + // TODO: add fg/bg stats and tag granularity + + private NetworkStats(long elapsedRealtime, String[] iface, int[] uid, long[] rx, long[] tx) { + this.elapsedRealtime = elapsedRealtime; + this.iface = iface; + this.uid = uid; + this.rx = rx; + this.tx = tx; + } + + public NetworkStats(Parcel parcel) { + elapsedRealtime = parcel.readLong(); + iface = parcel.createStringArray(); + uid = parcel.createIntArray(); + rx = parcel.createLongArray(); + tx = parcel.createLongArray(); + } + + public static class Builder { + private long mElapsedRealtime; + private final String[] mIface; + private final int[] mUid; + private final long[] mRx; + private final long[] mTx; + + private int mIndex = 0; + + public Builder(long elapsedRealtime, int size) { + mElapsedRealtime = elapsedRealtime; + mIface = new String[size]; + mUid = new int[size]; + mRx = new long[size]; + mTx = new long[size]; + } + + public void addEntry(String iface, int uid, long rx, long tx) { + mIface[mIndex] = iface; + mUid[mIndex] = uid; + mRx[mIndex] = rx; + mTx[mIndex] = tx; + mIndex++; + } + + public NetworkStats build() { + if (mIndex != mIface.length) { + throw new IllegalArgumentException("unexpected number of entries"); + } + return new NetworkStats(mElapsedRealtime, mIface, mUid, mRx, mTx); + } + } + + /** + * Find first stats index that matches the requested parameters. + */ + public int findIndex(String iface, int uid) { + for (int i = 0; i < this.iface.length; i++) { + if (equal(iface, this.iface[i]) && uid == this.uid[i]) { + return i; + } + } + return -1; + } + + private static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** {@inheritDoc} */ + public int describeContents() { + return 0; + } + + public void dump(String prefix, PrintWriter pw) { + pw.print(prefix); + pw.print("NetworkStats: elapsedRealtime="); pw.println(elapsedRealtime); + for (int i = 0; i < iface.length; i++) { + pw.print(prefix); + pw.print(" iface="); pw.print(iface[i]); + pw.print(" uid="); pw.print(uid[i]); + pw.print(" rx="); pw.print(rx[i]); + pw.print(" tx="); pw.println(tx[i]); + } + } + + @Override + public String toString() { + final CharArrayWriter writer = new CharArrayWriter(); + dump("", new PrintWriter(writer)); + return writer.toString(); + } + + /** {@inheritDoc} */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(elapsedRealtime); + dest.writeStringArray(iface); + dest.writeIntArray(uid); + dest.writeLongArray(rx); + dest.writeLongArray(tx); + } + + public static final Creator<NetworkStats> CREATOR = new Creator<NetworkStats>() { + public NetworkStats createFromParcel(Parcel in) { + return new NetworkStats(in); + } + + public NetworkStats[] newArray(int size) { + return new NetworkStats[size]; + } + }; +} diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java index f8f8a29..3bf64b2 100644 --- a/core/java/android/net/SSLCertificateSocketFactory.java +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -17,18 +17,12 @@ package android.net; import android.os.SystemProperties; -import android.util.Config; import android.util.Log; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; -import java.security.GeneralSecurityException; import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; import java.security.cert.X509Certificate; import javax.net.SocketFactory; @@ -40,7 +34,6 @@ import javax.net.ssl.SSLSession; 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 org.apache.harmony.xnet.provider.jsse.OpenSSLContextImpl; @@ -128,7 +121,7 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { * * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0 * for none. The socket timeout is reset to 0 after the handshake. - * @param cache The {@link SSLClientSessionCache} to use, or null for no cache. + * @param cache The {@link SSLSessionCache} to use, or null for no cache. * @return a new SSLSocketFactory with the specified parameters */ public static SSLSocketFactory getDefault(int handshakeTimeoutMillis, SSLSessionCache cache) { @@ -144,7 +137,7 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { * * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0 * for none. The socket timeout is reset to 0 after the handshake. - * @param cache The {@link SSLClientSessionCache} to use, or null for no cache. + * @param cache The {@link SSLSessionCache} to use, or null for no cache. * @return an insecure SSLSocketFactory with the specified parameters */ public static SSLSocketFactory getInsecure(int handshakeTimeoutMillis, SSLSessionCache cache) { @@ -157,12 +150,11 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { * * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0 * for none. The socket timeout is reset to 0 after the handshake. - * @param cache The {@link SSLClientSessionCache} to use, or null for no cache. + * @param cache The {@link SSLSessionCache} to use, or null for no cache. * @return a new SocketFactory with the specified parameters */ public static org.apache.http.conn.ssl.SSLSocketFactory getHttpSocketFactory( - int handshakeTimeoutMillis, - SSLSessionCache cache) { + int handshakeTimeoutMillis, SSLSessionCache cache) { return new org.apache.http.conn.ssl.SSLSocketFactory( new SSLCertificateSocketFactory(handshakeTimeoutMillis, cache, true)); } diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java index 3e21e2d..316440f 100644 --- a/core/java/android/net/SntpClient.java +++ b/core/java/android/net/SntpClient.java @@ -17,7 +17,6 @@ package android.net; import android.os.SystemClock; -import android.util.Config; import android.util.Log; import java.io.IOException; @@ -112,8 +111,8 @@ public class SntpClient // = (transit + skew - transit + skew)/2 // = (2 * skew)/2 = skew long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2; - // if (Config.LOGD) Log.d(TAG, "round trip: " + roundTripTime + " ms"); - // if (Config.LOGD) Log.d(TAG, "clock offset: " + clockOffset + " ms"); + // if (false) Log.d(TAG, "round trip: " + roundTripTime + " ms"); + // if (false) Log.d(TAG, "clock offset: " + clockOffset + " ms"); // save our results - use the times on this side of the network latency // (response rather than request time) @@ -121,7 +120,7 @@ public class SntpClient mNtpTimeReference = responseTicks; mRoundTripTime = roundTripTime; } catch (Exception e) { - if (Config.LOGD) Log.d(TAG, "request time failed: " + e); + if (false) Log.d(TAG, "request time failed: " + e); return false; } finally { if (socket != null) { diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java index eca06c5..7ee7a81 100644 --- a/core/java/android/net/TrafficStats.java +++ b/core/java/android/net/TrafficStats.java @@ -16,11 +16,10 @@ package android.net; -import android.util.Log; +import dalvik.system.BlockGuard; -import java.io.File; -import java.io.RandomAccessFile; -import java.io.IOException; +import java.net.Socket; +import java.net.SocketException; /** * Class that provides network traffic statistics. These statistics include @@ -37,6 +36,63 @@ public class TrafficStats { public final static int UNSUPPORTED = -1; /** + * Set active tag to use when accounting {@link Socket} traffic originating + * from the current thread. Only one active tag per thread is supported. + * <p> + * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + */ + public static void setThreadStatsTag(String tag) { + BlockGuard.setThreadSocketStatsTag(tag); + } + + public static void clearThreadStatsTag() { + BlockGuard.setThreadSocketStatsTag(null); + } + + /** + * Set specific UID to use when accounting {@link Socket} traffic + * originating from the current thread. Designed for use when performing an + * operation on behalf of another application. + * <p> + * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + * <p> + * To take effect, caller must hold + * {@link android.Manifest.permission#UPDATE_DEVICE_STATS} permission. + * + * {@hide} + */ + public static void setThreadStatsUid(int uid) { + BlockGuard.setThreadSocketStatsUid(uid); + } + + /** {@hide} */ + public static void clearThreadStatsUid() { + BlockGuard.setThreadSocketStatsUid(-1); + } + + /** + * Tag the given {@link Socket} with any statistics parameters active for + * the current thread. Subsequent calls always replace any existing + * parameters. When finished, call {@link #untagSocket(Socket)} to remove + * statistics parameters. + * + * @see #setThreadStatsTag(String) + * @see #setThreadStatsUid(int) + */ + public static void tagSocket(Socket socket) throws SocketException { + BlockGuard.tagSocketFd(socket.getFileDescriptor$()); + } + + /** + * Remove any statistics parameters from the given {@link Socket}. + */ + public static void untagSocket(Socket socket) throws SocketException { + BlockGuard.untagSocketFd(socket.getFileDescriptor$()); + } + + /** * Get the total number of packets transmitted through the mobile interface. * * @return number of packets. If the statistics are not supported by this device, diff --git a/core/java/android/net/http/Headers.java b/core/java/android/net/http/Headers.java index 74c0de8..657e071 100644 --- a/core/java/android/net/http/Headers.java +++ b/core/java/android/net/http/Headers.java @@ -16,7 +16,6 @@ package android.net.http; -import android.util.Config; import android.util.Log; import java.util.ArrayList; @@ -201,7 +200,7 @@ public final class Headers { try { contentLength = Long.parseLong(val); } catch (NumberFormatException e) { - if (Config.LOGV) { + if (false) { Log.v(LOGTAG, "Headers.headers(): error parsing" + " content length: " + buffer.toString()); } @@ -449,7 +448,7 @@ public final class Headers { } int extraLen = mExtraHeaderNames.size(); for (int i = 0; i < extraLen; i++) { - if (Config.LOGV) { + if (false) { HttpLog.v("Headers.getHeaders() extra: " + i + " " + mExtraHeaderNames.get(i) + " " + mExtraHeaderValues.get(i)); } diff --git a/core/java/android/net/http/HttpLog.java b/core/java/android/net/http/HttpLog.java index 30bf647..0934664 100644 --- a/core/java/android/net/http/HttpLog.java +++ b/core/java/android/net/http/HttpLog.java @@ -23,7 +23,6 @@ package android.net.http; import android.os.SystemClock; import android.util.Log; -import android.util.Config; /** * {@hide} @@ -32,7 +31,7 @@ class HttpLog { private final static String LOGTAG = "http"; private static final boolean DEBUG = false; - static final boolean LOGV = DEBUG ? Config.LOGD : Config.LOGV; + static final boolean LOGV = false; static void v(String logMe) { Log.v(LOGTAG, SystemClock.uptimeMillis() + " " + Thread.currentThread().getName() + " " + logMe); diff --git a/core/java/android/net/http/HttpsConnection.java b/core/java/android/net/http/HttpsConnection.java index d77e9d9..84765a5 100644 --- a/core/java/android/net/http/HttpsConnection.java +++ b/core/java/android/net/http/HttpsConnection.java @@ -289,11 +289,9 @@ public class HttpsConnection extends Connection { } else { // if we do not have a proxy, we simply connect to the host try { - sslSock = (SSLSocket) getSocketFactory().createSocket(); - + sslSock = (SSLSocket) getSocketFactory().createSocket( + mHost.getHostName(), mHost.getPort()); sslSock.setSoTimeout(SOCKET_TIMEOUT); - sslSock.connect(new InetSocketAddress(mHost.getHostName(), - mHost.getPort())); } catch(IOException e) { if (sslSock != null) { sslSock.close(); diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index 1803604..64bba54 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -153,7 +153,6 @@ public abstract class AsyncTask<Params, Progress, Result> { private static final int MAXIMUM_POOL_SIZE = 128; private static final int KEEP_ALIVE = 1; - private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); @@ -183,6 +182,7 @@ public abstract class AsyncTask<Params, Progress, Result> { private static final InternalHandler sHandler = new InternalHandler(); + private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; private final WorkerRunnable<Params, Result> mWorker; private final FutureTask<Result> mFuture; @@ -240,6 +240,11 @@ public abstract class AsyncTask<Params, Progress, Result> { sHandler.getLooper(); } + /** @hide */ + public static void setDefaultExecutor(Executor exec) { + sDefaultExecutor = exec; + } + /** * Creates a new asynchronous task. This constructor must be invoked on the UI thread. */ @@ -496,7 +501,7 @@ public abstract class AsyncTask<Params, Progress, Result> { * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. */ public final AsyncTask<Params, Progress, Result> execute(Params... params) { - return executeOnExecutor(THREAD_POOL_EXECUTOR, params); + return executeOnExecutor(sDefaultExecutor, params); } /** @@ -559,7 +564,7 @@ public abstract class AsyncTask<Params, Progress, Result> { * a simple Runnable object. */ public static void execute(Runnable runnable) { - THREAD_POOL_EXECUTOR.execute(runnable); + sDefaultExecutor.execute(runnable); } /** diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 1d6bc4e..e344197 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -26,6 +26,7 @@ import android.content.pm.ApplicationInfo; import android.telephony.SignalStrength; import android.util.Log; import android.util.Printer; +import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; @@ -408,15 +409,19 @@ public abstract class BatteryStats implements Parcelable { } public final static class HistoryItem implements Parcelable { + static final String TAG = "HistoryItem"; + static final boolean DEBUG = false; + public HistoryItem next; public long time; - public static final byte CMD_UPDATE = 0; - public static final byte CMD_START = 1; - public static final byte CMD_OVERFLOW = 2; + public static final byte CMD_NULL = 0; + public static final byte CMD_UPDATE = 1; + public static final byte CMD_START = 2; + public static final byte CMD_OVERFLOW = 3; - public byte cmd; + public byte cmd = CMD_NULL; public byte batteryLevel; public byte batteryStatus; @@ -427,33 +432,38 @@ public abstract class BatteryStats implements Parcelable { public char batteryVoltage; // Constants from SCREEN_BRIGHTNESS_* - public static final int STATE_BRIGHTNESS_MASK = 0x000000f; + public static final int STATE_BRIGHTNESS_MASK = 0x0000000f; public static final int STATE_BRIGHTNESS_SHIFT = 0; // Constants from SIGNAL_STRENGTH_* - public static final int STATE_SIGNAL_STRENGTH_MASK = 0x00000f0; + public static final int STATE_SIGNAL_STRENGTH_MASK = 0x000000f0; public static final int STATE_SIGNAL_STRENGTH_SHIFT = 4; // Constants from ServiceState.STATE_* - public static final int STATE_PHONE_STATE_MASK = 0x0000f00; + public static final int STATE_PHONE_STATE_MASK = 0x00000f00; public static final int STATE_PHONE_STATE_SHIFT = 8; // Constants from DATA_CONNECTION_* - public static final int STATE_DATA_CONNECTION_MASK = 0x000f000; + public static final int STATE_DATA_CONNECTION_MASK = 0x0000f000; public static final int STATE_DATA_CONNECTION_SHIFT = 12; - public static final int STATE_BATTERY_PLUGGED_FLAG = 1<<30; - public static final int STATE_SCREEN_ON_FLAG = 1<<29; + // These states always appear directly in the first int token + // of a delta change; they should be ones that change relatively + // frequently. + public static final int STATE_WAKE_LOCK_FLAG = 1<<30; + public static final int STATE_SENSOR_ON_FLAG = 1<<29; public static final int STATE_GPS_ON_FLAG = 1<<28; - public static final int STATE_PHONE_IN_CALL_FLAG = 1<<27; - public static final int STATE_PHONE_SCANNING_FLAG = 1<<26; - public static final int STATE_WIFI_ON_FLAG = 1<<25; - public static final int STATE_WIFI_RUNNING_FLAG = 1<<24; - public static final int STATE_WIFI_FULL_LOCK_FLAG = 1<<23; - public static final int STATE_WIFI_SCAN_LOCK_FLAG = 1<<22; - public static final int STATE_WIFI_MULTICAST_ON_FLAG = 1<<21; - public static final int STATE_BLUETOOTH_ON_FLAG = 1<<20; - public static final int STATE_AUDIO_ON_FLAG = 1<<19; - public static final int STATE_VIDEO_ON_FLAG = 1<<18; - public static final int STATE_WAKE_LOCK_FLAG = 1<<17; - public static final int STATE_SENSOR_ON_FLAG = 1<<16; + public static final int STATE_PHONE_SCANNING_FLAG = 1<<27; + public static final int STATE_WIFI_RUNNING_FLAG = 1<<26; + public static final int STATE_WIFI_FULL_LOCK_FLAG = 1<<25; + public static final int STATE_WIFI_SCAN_LOCK_FLAG = 1<<24; + public static final int STATE_WIFI_MULTICAST_ON_FLAG = 1<<23; + // These are on the lower bits used for the command; if they change + // we need to write another int of data. + public static final int STATE_AUDIO_ON_FLAG = 1<<22; + public static final int STATE_VIDEO_ON_FLAG = 1<<21; + public static final int STATE_SCREEN_ON_FLAG = 1<<20; + public static final int STATE_BATTERY_PLUGGED_FLAG = 1<<19; + public static final int STATE_PHONE_IN_CALL_FLAG = 1<<18; + public static final int STATE_WIFI_ON_FLAG = 1<<17; + public static final int STATE_BLUETOOTH_ON_FLAG = 1<<16; public static final int MOST_INTERESTING_STATES = STATE_BATTERY_PLUGGED_FLAG | STATE_SCREEN_ON_FLAG @@ -466,16 +476,7 @@ public abstract class BatteryStats implements Parcelable { public HistoryItem(long time, Parcel src) { this.time = time; - int bat = src.readInt(); - cmd = (byte)(bat&0xff); - batteryLevel = (byte)((bat>>8)&0xff); - batteryStatus = (byte)((bat>>16)&0xf); - batteryHealth = (byte)((bat>>20)&0xf); - batteryPlugType = (byte)((bat>>24)&0xf); - bat = src.readInt(); - batteryTemperature = (char)(bat&0xffff); - batteryVoltage = (char)((bat>>16)&0xffff); - states = src.readInt(); + readFromParcel(src); } public int describeContents() { @@ -495,6 +496,174 @@ public abstract class BatteryStats implements Parcelable { dest.writeInt(bat); dest.writeInt(states); } + + private void readFromParcel(Parcel src) { + int bat = src.readInt(); + cmd = (byte)(bat&0xff); + batteryLevel = (byte)((bat>>8)&0xff); + batteryStatus = (byte)((bat>>16)&0xf); + batteryHealth = (byte)((bat>>20)&0xf); + batteryPlugType = (byte)((bat>>24)&0xf); + bat = src.readInt(); + batteryTemperature = (char)(bat&0xffff); + batteryVoltage = (char)((bat>>16)&0xffff); + states = src.readInt(); + } + + // Part of initial delta int that specifies the time delta. + static final int DELTA_TIME_MASK = 0x3ffff; + static final int DELTA_TIME_ABS = 0x3fffd; // Following is an entire abs update. + static final int DELTA_TIME_INT = 0x3fffe; // The delta is a following int + static final int DELTA_TIME_LONG = 0x3ffff; // The delta is a following long + // Part of initial delta int holding the command code. + static final int DELTA_CMD_MASK = 0x3; + static final int DELTA_CMD_SHIFT = 18; + // Flag in delta int: a new battery level int follows. + static final int DELTA_BATTERY_LEVEL_FLAG = 1<<20; + // Flag in delta int: a new full state and battery status int follows. + static final int DELTA_STATE_FLAG = 1<<21; + static final int DELTA_STATE_MASK = 0xffc00000; + + public void writeDelta(Parcel dest, HistoryItem last) { + if (last == null || last.cmd != CMD_UPDATE) { + dest.writeInt(DELTA_TIME_ABS); + writeToParcel(dest, 0); + return; + } + + final long deltaTime = time - last.time; + final int lastBatteryLevelInt = last.buildBatteryLevelInt(); + final int lastStateInt = last.buildStateInt(); + + int deltaTimeToken; + if (deltaTime < 0 || deltaTime > Integer.MAX_VALUE) { + deltaTimeToken = DELTA_TIME_LONG; + } else if (deltaTime >= DELTA_TIME_ABS) { + deltaTimeToken = DELTA_TIME_INT; + } else { + deltaTimeToken = (int)deltaTime; + } + int firstToken = deltaTimeToken + | (cmd<<DELTA_CMD_SHIFT) + | (states&DELTA_STATE_MASK); + final int batteryLevelInt = buildBatteryLevelInt(); + final boolean batteryLevelIntChanged = batteryLevelInt != lastBatteryLevelInt; + if (batteryLevelIntChanged) { + firstToken |= DELTA_BATTERY_LEVEL_FLAG; + } + final int stateInt = buildStateInt(); + final boolean stateIntChanged = stateInt != lastStateInt; + if (stateIntChanged) { + firstToken |= DELTA_STATE_FLAG; + } + dest.writeInt(firstToken); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: firstToken=0x" + Integer.toHexString(firstToken) + + " deltaTime=" + deltaTime); + + if (deltaTimeToken >= DELTA_TIME_INT) { + if (deltaTimeToken == DELTA_TIME_INT) { + if (DEBUG) Slog.i(TAG, "WRITE DELTA: int deltaTime=" + (int)deltaTime); + dest.writeInt((int)deltaTime); + } else { + if (DEBUG) Slog.i(TAG, "WRITE DELTA: long deltaTime=" + deltaTime); + dest.writeLong(deltaTime); + } + } + if (batteryLevelIntChanged) { + dest.writeInt(batteryLevelInt); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: batteryToken=0x" + + Integer.toHexString(batteryLevelInt) + + " batteryLevel=" + batteryLevel + + " batteryTemp=" + (int)batteryTemperature + + " batteryVolt=" + (int)batteryVoltage); + } + if (stateIntChanged) { + dest.writeInt(stateInt); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: stateToken=0x" + + Integer.toHexString(stateInt) + + " batteryStatus=" + batteryStatus + + " batteryHealth=" + batteryHealth + + " batteryPlugType=" + batteryPlugType + + " states=0x" + Integer.toHexString(states)); + } + } + + private int buildBatteryLevelInt() { + return ((((int)batteryLevel)<<24)&0xff000000) + | ((((int)batteryTemperature)<<14)&0x00ffc000) + | (((int)batteryVoltage)&0x00003fff); + } + + private int buildStateInt() { + return ((((int)batteryStatus)<<28)&0xf0000000) + | ((((int)batteryHealth)<<24)&0x0f000000) + | ((((int)batteryPlugType)<<22)&0x00c00000) + | (states&(~DELTA_STATE_MASK)); + } + + public void readDelta(Parcel src) { + int firstToken = src.readInt(); + int deltaTimeToken = firstToken&DELTA_TIME_MASK; + cmd = (byte)((firstToken>>DELTA_CMD_SHIFT)&DELTA_CMD_MASK); + if (DEBUG) Slog.i(TAG, "READ DELTA: firstToken=0x" + Integer.toHexString(firstToken) + + " deltaTimeToken=" + deltaTimeToken); + + if (deltaTimeToken < DELTA_TIME_ABS) { + time += deltaTimeToken; + } else if (deltaTimeToken == DELTA_TIME_ABS) { + time = src.readLong(); + readFromParcel(src); + return; + } else if (deltaTimeToken == DELTA_TIME_INT) { + int delta = src.readInt(); + time += delta; + if (DEBUG) Slog.i(TAG, "READ DELTA: time delta=" + delta + " new time=" + time); + } else { + long delta = src.readLong(); + if (DEBUG) Slog.i(TAG, "READ DELTA: time delta=" + delta + " new time=" + time); + time += delta; + } + + if ((firstToken&DELTA_BATTERY_LEVEL_FLAG) != 0) { + int batteryLevelInt = src.readInt(); + batteryLevel = (byte)((batteryLevelInt>>24)&0xff); + batteryTemperature = (char)((batteryLevelInt>>14)&0x3ff); + batteryVoltage = (char)(batteryLevelInt&0x3fff); + if (DEBUG) Slog.i(TAG, "READ DELTA: batteryToken=0x" + + Integer.toHexString(batteryLevelInt) + + " batteryLevel=" + batteryLevel + + " batteryTemp=" + (int)batteryTemperature + + " batteryVolt=" + (int)batteryVoltage); + } + + if ((firstToken&DELTA_STATE_FLAG) != 0) { + int stateInt = src.readInt(); + states = (firstToken&DELTA_STATE_MASK) | (stateInt&(~DELTA_STATE_MASK)); + batteryStatus = (byte)((stateInt>>28)&0xf); + batteryHealth = (byte)((stateInt>>24)&0xf); + batteryPlugType = (byte)((stateInt>>22)&0x3); + if (DEBUG) Slog.i(TAG, "READ DELTA: stateToken=0x" + + Integer.toHexString(stateInt) + + " batteryStatus=" + batteryStatus + + " batteryHealth=" + batteryHealth + + " batteryPlugType=" + batteryPlugType + + " states=0x" + Integer.toHexString(states)); + } else { + states = (firstToken&DELTA_STATE_MASK) | (states&(~DELTA_STATE_MASK)); + } + } + + public void clear() { + time = 0; + cmd = CMD_NULL; + batteryLevel = 0; + batteryStatus = 0; + batteryHealth = 0; + batteryPlugType = 0; + batteryTemperature = 0; + batteryVoltage = 0; + states = 0; + } public void setTo(HistoryItem o) { time = o.time; @@ -556,11 +725,14 @@ public abstract class BatteryStats implements Parcelable { public abstract boolean getNextHistoryLocked(HistoryItem out); - /** - * Return the current history of battery state changes. - */ - public abstract HistoryItem getHistory(); - + public abstract void finishIteratingHistoryLocked(); + + public abstract boolean startIteratingOldHistoryLocked(); + + public abstract boolean getNextOldHistoryLocked(HistoryItem out); + + public abstract void finishIteratingOldHistoryLocked(); + /** * Return the base time offset for the battery history. */ @@ -1729,7 +1901,7 @@ public abstract class BatteryStats implements Parcelable { } } - void printBitDescriptions(PrintWriter pw, int oldval, int newval, BitDescription[] descriptions) { + static void printBitDescriptions(PrintWriter pw, int oldval, int newval, BitDescription[] descriptions) { int diff = oldval ^ newval; if (diff == 0) return; for (int i=0; i<descriptions.length; i++) { @@ -1753,6 +1925,125 @@ public abstract class BatteryStats implements Parcelable { } } + public void prepareForDumpLocked() { + } + + public static class HistoryPrinter { + int oldState = 0; + int oldStatus = -1; + int oldHealth = -1; + int oldPlug = -1; + int oldTemp = -1; + int oldVolt = -1; + + public void printNextItem(PrintWriter pw, HistoryItem rec, long now) { + pw.print(" "); + TimeUtils.formatDuration(rec.time-now, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN); + pw.print(" "); + if (rec.cmd == HistoryItem.CMD_START) { + pw.println(" START"); + } else if (rec.cmd == HistoryItem.CMD_OVERFLOW) { + pw.println(" *OVERFLOW*"); + } else { + if (rec.batteryLevel < 10) pw.print("00"); + else if (rec.batteryLevel < 100) pw.print("0"); + pw.print(rec.batteryLevel); + pw.print(" "); + if (rec.states < 0x10) pw.print("0000000"); + else if (rec.states < 0x100) pw.print("000000"); + else if (rec.states < 0x1000) pw.print("00000"); + else if (rec.states < 0x10000) pw.print("0000"); + else if (rec.states < 0x100000) pw.print("000"); + else if (rec.states < 0x1000000) pw.print("00"); + else if (rec.states < 0x10000000) pw.print("0"); + pw.print(Integer.toHexString(rec.states)); + if (oldStatus != rec.batteryStatus) { + oldStatus = rec.batteryStatus; + pw.print(" status="); + switch (oldStatus) { + case BatteryManager.BATTERY_STATUS_UNKNOWN: + pw.print("unknown"); + break; + case BatteryManager.BATTERY_STATUS_CHARGING: + pw.print("charging"); + break; + case BatteryManager.BATTERY_STATUS_DISCHARGING: + pw.print("discharging"); + break; + case BatteryManager.BATTERY_STATUS_NOT_CHARGING: + pw.print("not-charging"); + break; + case BatteryManager.BATTERY_STATUS_FULL: + pw.print("full"); + break; + default: + pw.print(oldStatus); + break; + } + } + if (oldHealth != rec.batteryHealth) { + oldHealth = rec.batteryHealth; + pw.print(" health="); + switch (oldHealth) { + case BatteryManager.BATTERY_HEALTH_UNKNOWN: + pw.print("unknown"); + break; + case BatteryManager.BATTERY_HEALTH_GOOD: + pw.print("good"); + break; + case BatteryManager.BATTERY_HEALTH_OVERHEAT: + pw.print("overheat"); + break; + case BatteryManager.BATTERY_HEALTH_DEAD: + pw.print("dead"); + break; + case BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE: + pw.print("over-voltage"); + break; + case BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE: + pw.print("failure"); + break; + default: + pw.print(oldHealth); + break; + } + } + if (oldPlug != rec.batteryPlugType) { + oldPlug = rec.batteryPlugType; + pw.print(" plug="); + switch (oldPlug) { + case 0: + pw.print("none"); + break; + case BatteryManager.BATTERY_PLUGGED_AC: + pw.print("ac"); + break; + case BatteryManager.BATTERY_PLUGGED_USB: + pw.print("usb"); + break; + default: + pw.print(oldPlug); + break; + } + } + if (oldTemp != rec.batteryTemperature) { + oldTemp = rec.batteryTemperature; + pw.print(" temp="); + pw.print(oldTemp); + } + if (oldVolt != rec.batteryVoltage) { + oldVolt = rec.batteryVoltage; + pw.print(" volt="); + pw.print(oldVolt); + } + printBitDescriptions(pw, oldState, rec.states, + HISTORY_STATE_DESCRIPTIONS); + pw.println(); + } + oldState = rec.states; + } + } + /** * Dumps a human-readable summary of the battery statistics to the given PrintWriter. * @@ -1760,122 +2051,28 @@ public abstract class BatteryStats implements Parcelable { */ @SuppressWarnings("unused") public void dumpLocked(PrintWriter pw) { + prepareForDumpLocked(); + + long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); + final HistoryItem rec = new HistoryItem(); if (startIteratingHistoryLocked()) { pw.println("Battery History:"); - long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); - int oldState = 0; - int oldStatus = -1; - int oldHealth = -1; - int oldPlug = -1; - int oldTemp = -1; - int oldVolt = -1; + HistoryPrinter hprinter = new HistoryPrinter(); while (getNextHistoryLocked(rec)) { - pw.print(" "); - TimeUtils.formatDuration(rec.time-now, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN); - pw.print(" "); - if (rec.cmd == HistoryItem.CMD_START) { - pw.println(" START"); - } else if (rec.cmd == HistoryItem.CMD_OVERFLOW) { - pw.println(" *OVERFLOW*"); - } else { - if (rec.batteryLevel < 10) pw.print("00"); - else if (rec.batteryLevel < 100) pw.print("0"); - pw.print(rec.batteryLevel); - pw.print(" "); - if (rec.states < 0x10) pw.print("0000000"); - else if (rec.states < 0x100) pw.print("000000"); - else if (rec.states < 0x1000) pw.print("00000"); - else if (rec.states < 0x10000) pw.print("0000"); - else if (rec.states < 0x100000) pw.print("000"); - else if (rec.states < 0x1000000) pw.print("00"); - else if (rec.states < 0x10000000) pw.print("0"); - pw.print(Integer.toHexString(rec.states)); - if (oldStatus != rec.batteryStatus) { - oldStatus = rec.batteryStatus; - pw.print(" status="); - switch (oldStatus) { - case BatteryManager.BATTERY_STATUS_UNKNOWN: - pw.print("unknown"); - break; - case BatteryManager.BATTERY_STATUS_CHARGING: - pw.print("charging"); - break; - case BatteryManager.BATTERY_STATUS_DISCHARGING: - pw.print("discharging"); - break; - case BatteryManager.BATTERY_STATUS_NOT_CHARGING: - pw.print("not-charging"); - break; - case BatteryManager.BATTERY_STATUS_FULL: - pw.print("full"); - break; - default: - pw.print(oldStatus); - break; - } - } - if (oldHealth != rec.batteryHealth) { - oldHealth = rec.batteryHealth; - pw.print(" health="); - switch (oldHealth) { - case BatteryManager.BATTERY_HEALTH_UNKNOWN: - pw.print("unknown"); - break; - case BatteryManager.BATTERY_HEALTH_GOOD: - pw.print("good"); - break; - case BatteryManager.BATTERY_HEALTH_OVERHEAT: - pw.print("overheat"); - break; - case BatteryManager.BATTERY_HEALTH_DEAD: - pw.print("dead"); - break; - case BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE: - pw.print("over-voltage"); - break; - case BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE: - pw.print("failure"); - break; - default: - pw.print(oldHealth); - break; - } - } - if (oldPlug != rec.batteryPlugType) { - oldPlug = rec.batteryPlugType; - pw.print(" plug="); - switch (oldPlug) { - case 0: - pw.print("none"); - break; - case BatteryManager.BATTERY_PLUGGED_AC: - pw.print("ac"); - break; - case BatteryManager.BATTERY_PLUGGED_USB: - pw.print("usb"); - break; - default: - pw.print(oldPlug); - break; - } - } - if (oldTemp != rec.batteryTemperature) { - oldTemp = rec.batteryTemperature; - pw.print(" temp="); - pw.print(oldTemp); - } - if (oldVolt != rec.batteryVoltage) { - oldVolt = rec.batteryVoltage; - pw.print(" volt="); - pw.print(oldVolt); - } - printBitDescriptions(pw, oldState, rec.states, - HISTORY_STATE_DESCRIPTIONS); - pw.println(); - } - oldState = rec.states; + hprinter.printNextItem(pw, rec, now); + } + finishIteratingHistoryLocked(); + pw.println(""); + } + + if (startIteratingOldHistoryLocked()) { + pw.println("Old battery History:"); + HistoryPrinter hprinter = new HistoryPrinter(); + while (getNextOldHistoryLocked(rec)) { + hprinter.printNextItem(pw, rec, now); } + finishIteratingOldHistoryLocked(); pw.println(""); } @@ -1917,6 +2114,8 @@ public abstract class BatteryStats implements Parcelable { @SuppressWarnings("unused") public void dumpCheckinLocked(PrintWriter pw, String[] args, List<ApplicationInfo> apps) { + prepareForDumpLocked(); + boolean isUnpluggedOnly = false; for (String arg : args) { diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index ae1e1c2..c25ebb7 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -16,7 +16,6 @@ package android.os; -import android.util.Config; import android.util.Log; import java.io.FileDescriptor; @@ -291,7 +290,7 @@ public class Binder implements IBinder { */ public final boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - if (Config.LOGV) Log.v("Binder", "Transact: " + code + " to " + this); + if (false) Log.v("Binder", "Transact: " + code + " to " + this); if (data != null) { data.setDataPosition(0); } @@ -413,7 +412,7 @@ final class BinderProxy implements IBinder { private native final void destroy(); private static final void sendDeathNotice(DeathRecipient recipient) { - if (Config.LOGV) Log.v("JavaBinder", "sendDeathNotice to " + recipient); + if (false) Log.v("JavaBinder", "sendDeathNotice to " + recipient); try { recipient.binderDied(); } diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 8735019..9accf99 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -235,6 +235,11 @@ public class Build { * Current development version. */ public static final int HONEYCOMB_MR2 = CUR_DEVELOPMENT; + + /** + * Current version under development. + */ + public static final int ICE_CREAM_SANDWICH = CUR_DEVELOPMENT; } /** The type of build, like "user" or "eng". */ diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 87aeccb..ba69246 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -18,7 +18,6 @@ package android.os; import com.android.internal.util.TypedProperties; -import android.util.Config; import android.util.Log; import java.io.FileDescriptor; @@ -1031,7 +1030,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo * Load the debug properties from the standard files into debugProperties. */ static { - if (Config.DEBUG) { + if (false) { final String TAG = "DebugProperties"; final String[] files = { "/system/debug.prop", "/debug.prop", "/data/debug.prop" }; final TypedProperties tp = new TypedProperties(); @@ -1157,10 +1156,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Reflectively sets static fields of a class based on internal debugging - * properties. This method is a no-op if android.util.Config.DEBUG is + * properties. This method is a no-op if false is * false. * <p> - * <strong>NOTE TO APPLICATION DEVELOPERS</strong>: Config.DEBUG will + * <strong>NOTE TO APPLICATION DEVELOPERS</strong>: false will * always be false in release builds. This API is typically only useful * for platform developers. * </p> @@ -1211,7 +1210,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo * the internal debugging property value. */ public static void setFieldsOn(Class<?> cl, boolean partial) { - if (Config.DEBUG) { + if (false) { if (debugProperties != null) { /* Only look for fields declared directly by the class, * so we don't mysteriously change static fields in superclasses. diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index 1f3f6d9..6b58877 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -16,13 +16,13 @@ package android.os; -import java.io.File; - import android.content.res.Resources; import android.os.storage.IMountService; import android.os.storage.StorageVolume; import android.util.Log; +import java.io.File; + /** * Provides access to environment variables. */ @@ -357,54 +357,54 @@ public class Environment { } /** - * getExternalStorageState() returns MEDIA_REMOVED if the media is not present. + * {@link #getExternalStorageState()} returns MEDIA_REMOVED if the media is not present. */ public static final String MEDIA_REMOVED = "removed"; /** - * getExternalStorageState() returns MEDIA_UNMOUNTED if the media is present + * {@link #getExternalStorageState()} returns MEDIA_UNMOUNTED if the media is present * but not mounted. */ public static final String MEDIA_UNMOUNTED = "unmounted"; /** - * getExternalStorageState() returns MEDIA_CHECKING if the media is present + * {@link #getExternalStorageState()} returns MEDIA_CHECKING if the media is present * and being disk-checked */ public static final String MEDIA_CHECKING = "checking"; /** - * getExternalStorageState() returns MEDIA_NOFS if the media is present + * {@link #getExternalStorageState()} returns MEDIA_NOFS if the media is present * but is blank or is using an unsupported filesystem */ public static final String MEDIA_NOFS = "nofs"; /** - * getExternalStorageState() returns MEDIA_MOUNTED if the media is present + * {@link #getExternalStorageState()} returns MEDIA_MOUNTED if the media is present * and mounted at its mount point with read/write access. */ public static final String MEDIA_MOUNTED = "mounted"; /** - * getExternalStorageState() returns MEDIA_MOUNTED_READ_ONLY if the media is present + * {@link #getExternalStorageState()} returns MEDIA_MOUNTED_READ_ONLY if the media is present * and mounted at its mount point with read only access. */ public static final String MEDIA_MOUNTED_READ_ONLY = "mounted_ro"; /** - * getExternalStorageState() returns MEDIA_SHARED if the media is present + * {@link #getExternalStorageState()} returns MEDIA_SHARED if the media is present * not mounted, and shared via USB mass storage. */ public static final String MEDIA_SHARED = "shared"; /** - * getExternalStorageState() returns MEDIA_BAD_REMOVAL if the media was + * {@link #getExternalStorageState()} returns MEDIA_BAD_REMOVAL if the media was * removed before it was unmounted. */ public static final String MEDIA_BAD_REMOVAL = "bad_removal"; /** - * getExternalStorageState() returns MEDIA_UNMOUNTABLE if the media is present + * {@link #getExternalStorageState()} returns MEDIA_UNMOUNTABLE if the media is present * but cannot be mounted. Typically this happens if the file system on the * media is corrupted. */ diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index 5a245f8..ecc111b 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -19,6 +19,7 @@ package android.os; import android.net.InterfaceConfiguration; import android.net.INetworkManagementEventObserver; +import android.net.NetworkStats; import android.net.RouteInfo; import android.net.wifi.WifiConfiguration; @@ -82,7 +83,6 @@ interface INetworkManagementService ** TETHERING RELATED **/ - /** * Returns true if IP forwarding is enabled */ @@ -198,17 +198,23 @@ interface INetworkManagementService void setAccessPoint(in WifiConfiguration wifiConfig, String wlanIface, String softapIface); /** - * Read number of bytes sent over an interface + ** DATA USAGE RELATED + **/ + + /** + * Return global network statistics summarized at an interface level, + * without any UID-level granularity. */ - long getInterfaceTxCounter(String iface); + NetworkStats getNetworkStatsSummary(); /** - * Read number of bytes received over an interface + * Return detailed network statistics with UID-level granularity, + * including interface and tag details. */ - long getInterfaceRxCounter(String iface); + NetworkStats getNetworkStatsDetail(); /** - * Configures bandwidth throttling on an interface + * Configures bandwidth throttling on an interface. */ void setInterfaceThrottle(String iface, int rxKbps, int txKbps); diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java index ccf642c..3edd692 100644 --- a/core/java/android/os/Looper.java +++ b/core/java/android/os/Looper.java @@ -16,7 +16,6 @@ package android.os; -import android.util.Config; import android.util.Log; import android.util.Printer; import android.util.PrefixPrinter; diff --git a/core/java/android/os/MemoryFile.java b/core/java/android/os/MemoryFile.java index f82702a..e8148f7 100644 --- a/core/java/android/os/MemoryFile.java +++ b/core/java/android/os/MemoryFile.java @@ -28,7 +28,7 @@ import java.io.OutputStream; * MemoryFile is a wrapper for the Linux ashmem driver. * MemoryFiles are backed by shared memory, which can be optionally * set to be purgeable. - * Purgeable files may have their contents reclaimed by the kernel + * Purgeable files may have their contents reclaimed by the kernel * in low memory conditions (only if allowPurging is set to true). * After a file is purged, attempts to read or write the file will * cause an IOException to be thrown. @@ -126,7 +126,7 @@ public class MemoryFile close(); } } - + /** * Returns the length of the memory file. * @@ -190,7 +190,7 @@ public class MemoryFile * @return number of bytes read. * @throws IOException if the memory file has been purged or deactivated. */ - public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count) + public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count) throws IOException { if (isDeactivated()) { throw new IOException("Can't read from deactivated memory file."); @@ -330,6 +330,7 @@ public class MemoryFile @Override public void write(byte buffer[], int offset, int count) throws IOException { writeBytes(buffer, offset, mOffset, count); + mOffset += count; } @Override diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java index bb07825..a658fc4 100644 --- a/core/java/android/os/MessageQueue.java +++ b/core/java/android/os/MessageQueue.java @@ -17,7 +17,6 @@ package android.os; import android.util.AndroidRuntimeException; -import android.util.Config; import android.util.Log; import java.util.ArrayList; @@ -128,7 +127,7 @@ public class MessageQueue { mBlocked = false; mMessages = msg.next; msg.next = null; - if (Config.LOGV) Log.v("MessageQueue", "Returning message: " + msg); + if (false) Log.v("MessageQueue", "Returning message: " + msg); msg.markInUse(); return msg; } else { diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index aa959b4..727fcca 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -21,6 +21,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.net.DatagramSocket; import java.net.Socket; /** @@ -35,64 +36,64 @@ public class ParcelFileDescriptor implements Parcelable { //consider ParcelFileDescriptor A(fileDescriptor fd), ParcelFileDescriptor B(A) //in this particular case fd.close might be invoked twice. private final ParcelFileDescriptor mParcelDescriptor; - + /** * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied * and this file doesn't already exist, then create the file with * permissions such that any application can read it. */ public static final int MODE_WORLD_READABLE = 0x00000001; - + /** * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied * and this file doesn't already exist, then create the file with * permissions such that any application can write it. */ public static final int MODE_WORLD_WRITEABLE = 0x00000002; - + /** * For use with {@link #open}: open the file with read-only access. */ public static final int MODE_READ_ONLY = 0x10000000; - + /** * For use with {@link #open}: open the file with write-only access. */ public static final int MODE_WRITE_ONLY = 0x20000000; - + /** * For use with {@link #open}: open the file with read and write access. */ public static final int MODE_READ_WRITE = 0x30000000; - + /** * For use with {@link #open}: create the file if it doesn't already exist. */ public static final int MODE_CREATE = 0x08000000; - + /** * For use with {@link #open}: erase contents of file when opening. */ 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. * @param mode The desired access mode, must be one of * {@link #MODE_READ_ONLY}, {@link #MODE_WRITE_ONLY}, or * {@link #MODE_READ_WRITE}; may also be any combination of * {@link #MODE_CREATE}, {@link #MODE_TRUNCATE}, * {@link #MODE_WORLD_READABLE}, and {@link #MODE_WORLD_WRITEABLE}. - * + * * @return Returns a new ParcelFileDescriptor pointing to the given * file. - * + * * @throws FileNotFoundException Throws FileNotFoundException if the given * file does not exist or can not be opened with the requested mode. */ @@ -106,12 +107,12 @@ public class ParcelFileDescriptor implements Parcelable { security.checkWrite(path); } } - + if ((mode&MODE_READ_WRITE) == 0) { throw new IllegalArgumentException( "Must specify MODE_READ_ONLY, MODE_WRITE_ONLY, or MODE_READ_WRITE"); } - + FileDescriptor fd = Parcel.openFileDescriptor(path, mode); return fd != null ? new ParcelFileDescriptor(fd) : null; } @@ -137,12 +138,23 @@ public class ParcelFileDescriptor implements Parcelable { * specified Socket. */ public static ParcelFileDescriptor fromSocket(Socket socket) { - FileDescriptor fd = getFileDescriptorFromSocket(socket); + FileDescriptor fd = socket.getFileDescriptor$(); return fd != null ? new ParcelFileDescriptor(fd) : null; } - // Extracts the file descriptor from the specified socket and returns it untouched - private static native FileDescriptor getFileDescriptorFromSocket(Socket socket); + /** + * Create a new ParcelFileDescriptor from the specified DatagramSocket. + * + * @param datagramSocket The DatagramSocket whose FileDescriptor is used + * to create a new ParcelFileDescriptor. + * + * @return A new ParcelFileDescriptor with the FileDescriptor of the + * specified DatagramSocket. + */ + public static ParcelFileDescriptor fromDatagramSocket(DatagramSocket datagramSocket) { + FileDescriptor fd = datagramSocket.getFileDescriptor$(); + return fd != null ? new ParcelFileDescriptor(fd) : null; + } /** * Create two ParcelFileDescriptors structured as a data pipe. The first @@ -187,26 +199,26 @@ public class ParcelFileDescriptor implements Parcelable { /** * Retrieve the actual FileDescriptor associated with this object. - * + * * @return Returns the FileDescriptor associated with this object. */ public FileDescriptor getFileDescriptor() { return mFileDescriptor; } - + /** * 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); - + /** * Return the native fd int for this ParcelFileDescriptor. The * ParcelFileDescriptor still owns the fd, and it still must be closed @@ -218,9 +230,9 @@ public class ParcelFileDescriptor implements Parcelable { } return getFdNative(); } - + private native int getFdNative(); - + /** * Return the native fd int for this ParcelFileDescriptor and detach it * from the object here. You are now responsible for closing the fd in @@ -240,11 +252,11 @@ public class ParcelFileDescriptor implements Parcelable { Parcel.clearFileDescriptor(mFileDescriptor); return fd; } - + /** * Close the ParcelFileDescriptor. This implementation closes the underlying * OS resources allocated to represent this stream. - * + * * @throws IOException * If an error occurs attempting to close this ParcelFileDescriptor. */ @@ -261,7 +273,7 @@ public class ParcelFileDescriptor implements Parcelable { Parcel.closeFileDescriptor(mFileDescriptor); } } - + /** * An InputStream you can create on a ParcelFileDescriptor, which will * take care of calling {@link ParcelFileDescriptor#close @@ -269,7 +281,7 @@ public class ParcelFileDescriptor implements Parcelable { */ public static class AutoCloseInputStream extends FileInputStream { private final ParcelFileDescriptor mFd; - + public AutoCloseInputStream(ParcelFileDescriptor fd) { super(fd.getFileDescriptor()); mFd = fd; @@ -284,7 +296,7 @@ public class ParcelFileDescriptor implements Parcelable { } } } - + /** * An OutputStream you can create on a ParcelFileDescriptor, which will * take care of calling {@link ParcelFileDescriptor#close @@ -292,7 +304,7 @@ public class ParcelFileDescriptor implements Parcelable { */ public static class AutoCloseOutputStream extends FileOutputStream { private final ParcelFileDescriptor mFd; - + public AutoCloseOutputStream(ParcelFileDescriptor fd) { super(fd.getFileDescriptor()); mFd = fd; @@ -307,12 +319,12 @@ public class ParcelFileDescriptor implements Parcelable { } } } - + @Override public String toString() { return "{ParcelFileDescriptor: " + mFileDescriptor + "}"; } - + @Override protected void finalize() throws Throwable { try { @@ -323,13 +335,13 @@ public class ParcelFileDescriptor implements Parcelable { super.finalize(); } } - + public ParcelFileDescriptor(ParcelFileDescriptor descriptor) { super(); mParcelDescriptor = descriptor; mFileDescriptor = mParcelDescriptor.mFileDescriptor; } - + /*package */ParcelFileDescriptor(FileDescriptor descriptor) { super(); if (descriptor == null) { @@ -338,7 +350,7 @@ public class ParcelFileDescriptor implements Parcelable { mFileDescriptor = descriptor; mParcelDescriptor = null; } - + /* Parcelable interface */ public int describeContents() { return Parcelable.CONTENTS_FILE_DESCRIPTOR; diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 78275a4..a17983a 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -97,7 +97,8 @@ import android.util.Log; * </tbody> * </table> * - * + * Any application using a WakeLock must request the {@code android.permission.WAKE_LOCK} + * permission in an {@code <uses-permission>} element of the application's manifest. */ public class PowerManager { @@ -199,8 +200,11 @@ public class PowerManager /** * Class lets you say that you need to have the device on. - * - * <p>Call release when you are done and don't need the lock anymore. + * <p> + * Call release when you are done and don't need the lock anymore. + * <p> + * Any application using a WakeLock must request the {@code android.permission.WAKE_LOCK} + * permission in an {@code <uses-permission>} element of the application's manifest. */ public class WakeLock { @@ -257,16 +261,10 @@ public class PowerManager public void acquire() { synchronized (mToken) { - if (!mRefCounted || mCount++ == 0) { - try { - mService.acquireWakeLock(mFlags, mToken, mTag, mWorkSource); - } catch (RemoteException e) { - } - mHeld = true; - } + acquireLocked(); } } - + /** * Makes sure the device is on at the level you asked when you created * the wake lock. The lock will be released after the given timeout. @@ -274,10 +272,22 @@ public class PowerManager * @param timeout Release the lock after the give timeout in milliseconds. */ public void acquire(long timeout) { - acquire(); - mHandler.postDelayed(mReleaser, timeout); + synchronized (mToken) { + acquireLocked(); + mHandler.postDelayed(mReleaser, timeout); + } } + private void acquireLocked() { + if (!mRefCounted || mCount++ == 0) { + mHandler.removeCallbacks(mReleaser); + try { + mService.acquireWakeLock(mFlags, mToken, mTag, mWorkSource); + } catch (RemoteException e) { + } + mHeld = true; + } + } /** * Release your claim to the CPU or screen being on. @@ -286,8 +296,7 @@ public class PowerManager * It may turn off shortly after you release it, or it may not if there * are other wake locks held. */ - public void release() - { + public void release() { release(0); } @@ -302,9 +311,9 @@ public class PowerManager * * {@hide} */ - public void release(int flags) - { + public void release(int flags) { synchronized (mToken) { + mHandler.removeCallbacks(mReleaser); if (!mRefCounted || --mCount == 0) { try { mService.releaseWakeLock(mToken, flags); diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 2bfada0..f85df6c 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -92,6 +92,12 @@ public class Process { public static final int SDCARD_RW_GID = 1015; /** + * Defines the UID for the KeyChain service. + * @hide + */ + public static final int KEYCHAIN_UID = 1020; + + /** * Defines the UID/GID for the NFC service process. * @hide */ diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index c1dd911..ae605fb 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -70,7 +70,7 @@ public class RecoverySystem { private static File RECOVERY_DIR = new File("/cache/recovery"); private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); private static File LOG_FILE = new File(RECOVERY_DIR, "log"); - private static String LAST_LOG_FILENAME = "last_log"; + private static String LAST_PREFIX = "last_"; // Length limits for reading files. private static int LOG_FILE_MAX_LENGTH = 64 * 1024; @@ -415,10 +415,11 @@ public class RecoverySystem { Log.e(TAG, "Error reading recovery log", e); } - // Delete everything in RECOVERY_DIR except LAST_LOG_FILENAME + // Delete everything in RECOVERY_DIR except those beginning + // with LAST_PREFIX String[] names = RECOVERY_DIR.list(); for (int i = 0; names != null && i < names.length; i++) { - if (names[i].equals(LAST_LOG_FILENAME)) continue; + if (names[i].startsWith(LAST_PREFIX)) continue; File f = new File(RECOVERY_DIR, names[i]); if (!f.delete()) { Log.e(TAG, "Can't delete: " + f); diff --git a/core/java/android/pim/ICalendar.java b/core/java/android/pim/ICalendar.java index 9c4eaf4..58c5c63 100644 --- a/core/java/android/pim/ICalendar.java +++ b/core/java/android/pim/ICalendar.java @@ -17,7 +17,6 @@ package android.pim; import android.util.Log; -import android.util.Config; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -447,7 +446,7 @@ public class ICalendar { component = current; } } catch (FormatException fe) { - if (Config.LOGV) { + if (false) { Log.v(TAG, "Cannot parse " + line, fe); } // for now, we ignore the parse error. Google Calendar seems diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java index 282417d..fdd0783 100644 --- a/core/java/android/pim/RecurrenceSet.java +++ b/core/java/android/pim/RecurrenceSet.java @@ -21,7 +21,6 @@ import android.database.Cursor; import android.provider.Calendar; import android.text.TextUtils; import android.text.format.Time; -import android.util.Config; import android.util.Log; import java.util.List; @@ -197,7 +196,7 @@ public class RecurrenceSet { (TextUtils.isEmpty(duration))|| ((TextUtils.isEmpty(rrule))&& (TextUtils.isEmpty(rdate)))) { - if (Config.LOGD) { + if (false) { Log.d(TAG, "Recurrence missing DTSTART, DTEND/DURATION, " + "or RRULE/RDATE: " + component.toString()); @@ -211,7 +210,7 @@ public class RecurrenceSet { long millis = start.toMillis(false /* use isDst */); values.put(Calendar.Events.DTSTART, millis); if (millis == -1) { - if (Config.LOGD) { + if (false) { Log.d(TAG, "DTSTART is out of range: " + component.toString()); } return false; diff --git a/core/java/android/preference/MultiSelectListPreference.java b/core/java/android/preference/MultiSelectListPreference.java index 42d555c..2e8d551 100644 --- a/core/java/android/preference/MultiSelectListPreference.java +++ b/core/java/android/preference/MultiSelectListPreference.java @@ -169,9 +169,9 @@ public class MultiSelectListPreference extends DialogPreference { new DialogInterface.OnMultiChoiceClickListener() { public void onClick(DialogInterface dialog, int which, boolean isChecked) { if (isChecked) { - mPreferenceChanged |= mNewValues.add(mEntries[which].toString()); + mPreferenceChanged |= mNewValues.add(mEntryValues[which].toString()); } else { - mPreferenceChanged |= mNewValues.remove(mEntries[which].toString()); + mPreferenceChanged |= mNewValues.remove(mEntryValues[which].toString()); } } }); @@ -180,7 +180,7 @@ public class MultiSelectListPreference extends DialogPreference { } private boolean[] getSelectedItems() { - final CharSequence[] entries = mEntries; + final CharSequence[] entries = mEntryValues; final int entryCount = entries.length; final Set<String> values = mValues; boolean[] result = new boolean[entryCount]; diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index 7d37e5b..5e1be21 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -89,6 +89,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis private int mOrder = DEFAULT_ORDER; private CharSequence mTitle; + private int mTitleRes; private CharSequence mSummary; /** * mIconResId is overridden by mIcon, if mIcon is specified. @@ -214,6 +215,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis break; case com.android.internal.R.styleable.Preference_title: + mTitleRes = a.getResourceId(attr, 0); mTitle = a.getString(attr); break; @@ -582,6 +584,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis */ public void setTitle(CharSequence title) { if (title == null && mTitle != null || title != null && !title.equals(mTitle)) { + mTitleRes = 0; mTitle = title; notifyChanged(); } @@ -595,9 +598,21 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis */ public void setTitle(int titleResId) { setTitle(mContext.getString(titleResId)); + mTitleRes = titleResId; } /** + * Returns the title resource ID of this Preference. If the title did + * not come from a resource, 0 is returned. + * + * @return The title resource. + * @see #setTitle(int) + */ + public int getTitleRes() { + return mTitleRes; + } + + /** * Returns the title of this Preference. * * @return The title. diff --git a/core/java/android/preference/PreferenceActivity.java b/core/java/android/preference/PreferenceActivity.java index ad0bc84..15d5898 100644 --- a/core/java/android/preference/PreferenceActivity.java +++ b/core/java/android/preference/PreferenceActivity.java @@ -132,13 +132,28 @@ public abstract class PreferenceActivity extends ListActivity implements /** * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, - * this extra can also be specify to supply a Bundle of arguments to pass + * this extra can also be specified to supply a Bundle of arguments to pass * to that fragment when it is instantiated during the initial creation * of PreferenceActivity. */ public static final String EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":android:show_fragment_args"; /** + * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, + * this extra can also be specify to supply the title to be shown for + * that fragment. + */ + public static final String EXTRA_SHOW_FRAGMENT_TITLE = ":android:show_fragment_title"; + + /** + * When starting this activity and using {@link #EXTRA_SHOW_FRAGMENT}, + * this extra can also be specify to supply the short title to be shown for + * that fragment. + */ + public static final String EXTRA_SHOW_FRAGMENT_SHORT_TITLE + = ":android:show_fragment_short_title"; + + /** * When starting this activity, the invoking Intent can contain this extra * boolean that the header list should not be displayed. This is most often * used in conjunction with {@link #EXTRA_SHOW_FRAGMENT} to launch @@ -488,7 +503,12 @@ public abstract class PreferenceActivity extends ListActivity implements protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(com.android.internal.R.layout.preference_list_content); + if (getResources().getConfiguration().isLayoutSizeAtLeast( + Configuration.SCREENLAYOUT_SIZE_LARGE)) { + setContentView(com.android.internal.R.layout.preference_list_content_large); + } else { + setContentView(com.android.internal.R.layout.preference_list_content); + } mListFooter = (FrameLayout)findViewById(com.android.internal.R.id.list_footer); mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs_frame); @@ -496,6 +516,8 @@ public abstract class PreferenceActivity extends ListActivity implements mSinglePane = hidingHeaders || !onIsMultiPane(); String initialFragment = getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT); Bundle initialArguments = getIntent().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS); + int initialTitle = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_TITLE, 0); + int initialShortTitle = getIntent().getIntExtra(EXTRA_SHOW_FRAGMENT_SHORT_TITLE, 0); if (savedInstanceState != null) { // We are restarting from a previous saved state; used that to @@ -516,6 +538,12 @@ public abstract class PreferenceActivity extends ListActivity implements // new fragment mode, but don't need to compute and show // the headers. switchToHeader(initialFragment, initialArguments); + if (initialTitle != 0) { + CharSequence initialTitleStr = getText(initialTitle); + CharSequence initialShortTitleStr = initialShortTitle != 0 + ? getText(initialShortTitle) : null; + showBreadCrumbs(initialTitleStr, initialShortTitleStr); + } } else { // We need to try to build the headers. @@ -557,7 +585,12 @@ public abstract class PreferenceActivity extends ListActivity implements } else { // If there are no headers, we are in the old "just show a screen // of preferences" mode. - setContentView(com.android.internal.R.layout.preference_list_content_single); + if (getResources().getConfiguration().isLayoutSizeAtLeast( + Configuration.SCREENLAYOUT_SIZE_LARGE)) { + setContentView(com.android.internal.R.layout.preference_list_content_single_large); + } else { + setContentView(com.android.internal.R.layout.preference_list_content_single); + } mListFooter = (FrameLayout) findViewById(com.android.internal.R.id.list_footer); mPrefsContainer = (ViewGroup) findViewById(com.android.internal.R.id.prefs); mPreferenceManager = new PreferenceManager(this, FIRST_REQUEST_CODE); @@ -942,7 +975,8 @@ public abstract class PreferenceActivity extends ListActivity implements /** * Called when the user selects an item in the header list. The default - * implementation will call either {@link #startWithFragment(String, Bundle, Fragment, int)} + * implementation will call either + * {@link #startWithFragment(String, Bundle, Fragment, int, int, int)} * or {@link #switchToHeader(Header)} as appropriate. * * @param header The header that was selected. @@ -951,7 +985,14 @@ public abstract class PreferenceActivity extends ListActivity implements public void onHeaderClick(Header header, int position) { if (header.fragment != null) { if (mSinglePane) { - startWithFragment(header.fragment, header.fragmentArguments, null, 0); + int titleRes = header.breadCrumbTitleRes; + int shortTitleRes = header.breadCrumbShortTitleRes; + if (titleRes == 0) { + titleRes = header.titleRes; + shortTitleRes = 0; + } + startWithFragment(header.fragment, header.fragmentArguments, null, 0, + titleRes, shortTitleRes); } else { switchToHeader(header); } @@ -961,6 +1002,41 @@ public abstract class PreferenceActivity extends ListActivity implements } /** + * Called by {@link #startWithFragment(String, Bundle, Fragment, int, int, int)} when + * in single-pane mode, to build an Intent to launch a new activity showing + * the selected fragment. The default implementation constructs an Intent + * that re-launches the current activity with the appropriate arguments to + * display the fragment. + * + * @param fragmentName The name of the fragment to display. + * @param args Optional arguments to supply to the fragment. + * @param titleRes Optional resource ID of title to show for this item. + * @param titleRes Optional resource ID of short title to show for this item. + * @return Returns an Intent that can be launched to display the given + * fragment. + */ + public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args, + int titleRes, int shortTitleRes) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClass(this, getClass()); + intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName); + intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); + intent.putExtra(EXTRA_SHOW_FRAGMENT_TITLE, titleRes); + intent.putExtra(EXTRA_SHOW_FRAGMENT_SHORT_TITLE, shortTitleRes); + intent.putExtra(EXTRA_NO_HEADERS, true); + return intent; + } + + /** + * Like {@link #startWithFragment(String, Bundle, Fragment, int, int, int)} + * but uses a 0 titleRes. + */ + public void startWithFragment(String fragmentName, Bundle args, + Fragment resultTo, int resultRequestCode) { + startWithFragment(fragmentName, args, resultTo, resultRequestCode, 0, 0); + } + + /** * Start a new instance of this activity, showing only the given * preference fragment. When launched in this mode, the header list * will be hidden and the given preference fragment will be instantiated @@ -968,14 +1044,18 @@ public abstract class PreferenceActivity extends ListActivity implements * * @param fragmentName The name of the fragment to display. * @param args Optional arguments to supply to the fragment. + * @param resultTo Option fragment that should receive the result of + * the activity launch. + * @param resultRequestCode If resultTo is non-null, this is the request + * code in which to report the result. + * @param titleRes Resource ID of string to display for the title of + * this set of preferences. + * @param titleRes Resource ID of string to display for the short title of + * this set of preferences. */ public void startWithFragment(String fragmentName, Bundle args, - Fragment resultTo, int resultRequestCode) { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClass(this, getClass()); - intent.putExtra(EXTRA_SHOW_FRAGMENT, fragmentName); - intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args); - intent.putExtra(EXTRA_NO_HEADERS, true); + Fragment resultTo, int resultRequestCode, int titleRes, int shortTitleRes) { + Intent intent = onBuildStartFragmentIntent(fragmentName, args, titleRes, shortTitleRes); if (resultTo == null) { startActivity(intent); } else { @@ -992,16 +1072,16 @@ public abstract class PreferenceActivity extends ListActivity implements if (mFragmentBreadCrumbs == null) { View crumbs = findViewById(android.R.id.title); // For screens with a different kind of title, don't create breadcrumbs. - if (!(crumbs instanceof FragmentBreadCrumbs)) return; - mFragmentBreadCrumbs = (FragmentBreadCrumbs) findViewById(android.R.id.title); + try { + mFragmentBreadCrumbs = (FragmentBreadCrumbs)crumbs; + } catch (ClassCastException e) { + return; + } if (mFragmentBreadCrumbs == null) { - mFragmentBreadCrumbs = new FragmentBreadCrumbs(this); - ActionBar actionBar = getActionBar(); - if (actionBar != null) { - actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM, - ActionBar.DISPLAY_SHOW_TITLE | ActionBar.DISPLAY_SHOW_CUSTOM); - actionBar.setCustomView(mFragmentBreadCrumbs); + if (title != null) { + setTitle(title); } + return; } mFragmentBreadCrumbs.setMaxVisible(2); mFragmentBreadCrumbs.setActivity(this); @@ -1169,7 +1249,7 @@ public abstract class PreferenceActivity extends ListActivity implements public void startPreferencePanel(String fragmentClass, Bundle args, int titleRes, CharSequence titleText, Fragment resultTo, int resultRequestCode) { if (mSinglePane) { - startWithFragment(fragmentClass, args, resultTo, resultRequestCode); + startWithFragment(fragmentClass, args, resultTo, resultRequestCode, titleRes, 0); } else { Fragment f = Fragment.instantiate(this, fragmentClass, args); if (resultTo != null) { @@ -1215,7 +1295,8 @@ public abstract class PreferenceActivity extends ListActivity implements @Override public boolean onPreferenceStartFragment(PreferenceFragment caller, Preference pref) { - startPreferencePanel(pref.getFragment(), pref.getExtras(), 0, pref.getTitle(), null, 0); + startPreferencePanel(pref.getFragment(), pref.getExtras(), pref.getTitleRes(), + pref.getTitle(), null, 0); return true; } diff --git a/core/java/android/preference/PreferenceFragment.java b/core/java/android/preference/PreferenceFragment.java index 4e22ba0..7511e14 100644 --- a/core/java/android/preference/PreferenceFragment.java +++ b/core/java/android/preference/PreferenceFragment.java @@ -20,6 +20,7 @@ import android.app.Activity; import android.app.Fragment; import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Configuration; import android.os.Bundle; import android.os.Handler; import android.os.Message; @@ -151,8 +152,14 @@ public abstract class PreferenceFragment extends Fragment implements @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(com.android.internal.R.layout.preference_list_fragment, - container, false); + if (getResources().getConfiguration().isLayoutSizeAtLeast( + Configuration.SCREENLAYOUT_SIZE_LARGE)) { + return inflater.inflate(com.android.internal.R.layout.preference_list_fragment_large, + container, false); + } else { + return inflater.inflate(com.android.internal.R.layout.preference_list_fragment, + container, false); + } } @Override diff --git a/core/java/android/provider/BrowserContract.java b/core/java/android/provider/BrowserContract.java index 03bc41a..d678205 100644 --- a/core/java/android/provider/BrowserContract.java +++ b/core/java/android/provider/BrowserContract.java @@ -332,6 +332,13 @@ public class BrowserContract { * <P>Type: TEXT</P> */ public static final String ACCOUNT_TYPE = "account_type"; + + /** + * The ID of the account's root folder. This will be the ID of the folder + * returned when querying {@link Bookmarks#CONTENT_URI_DEFAULT_FOLDER}. + * <P>Type: INTEGER</P> + */ + public static final String ROOT_ID = "root_id"; } /** diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index de71763..4141879 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -98,6 +98,8 @@ public final class Calendar { public static final String SYNC4 = "sync4"; /** Generic column for use by sync adapters. */ public static final String SYNC5 = "sync5"; + /** Generic column for use by sync adapters. */ + public static final String SYNC6 = "sync6"; } /** @@ -108,13 +110,13 @@ public final class Calendar { * The account that was used to sync the entry to the device. * <P>Type: TEXT</P> */ - public static final String _SYNC_ACCOUNT = "_sync_account"; + public static final String ACCOUNT_NAME = "account_name"; /** * The type of the account that was used to sync the entry to the device. * <P>Type: TEXT</P> */ - public static final String _SYNC_ACCOUNT_TYPE = "_sync_account_type"; + public static final String ACCOUNT_TYPE = "account_type"; /** * The unique ID for a row assigned by the sync source. NULL if the row has never been synced. @@ -135,15 +137,6 @@ public final class Calendar { public static final String _SYNC_VERSION = "_sync_version"; /** - * For use by sync adapter at its discretion; not modified by CalendarProvider - * Note that this column was formerly named _SYNC_LOCAL_ID. We are using it to avoid a - * schema change. - * TODO Replace this with something more general in the future. - * <P>Type: INTEGER (long)</P> - */ - public static final String _SYNC_DATA = "_sync_local_id"; - - /** * Used only in persistent providers, and only during merging. * <P>Type: INTEGER (long)</P> */ @@ -153,30 +146,11 @@ public final class Calendar { * Used to indicate that local, unsynced, changes are present. * <P>Type: INTEGER (long)</P> */ - public static final String _SYNC_DIRTY = "_sync_dirty"; + public static final String DIRTY = "dirty"; } /** - * Columns from the Account information used by Calendars and Events tables. - */ - public interface AccountColumns { - /** - * The name of the account instance to which this row belongs, which when paired with - * {@link #ACCOUNT_TYPE} identifies a specific account. - * <P>Type: TEXT</P> - */ - public static final String ACCOUNT_NAME = "account_name"; - - /** - * The type of account to which this row belongs, which when paired with - * {@link #ACCOUNT_NAME} identifies a specific account. - * <P>Type: TEXT</P> - */ - public static final String ACCOUNT_TYPE = "account_type"; - } - - /** * Columns from the Calendars table that other tables join into themselves. */ public interface CalendarsColumns { @@ -184,7 +158,7 @@ public final class Calendar { * The color of the calendar * <P>Type: INTEGER (color value)</P> */ - public static final String COLOR = "color"; + public static final String CALENDAR_COLOR = "calendar_color"; /** * The level of access that the user has for the calendar @@ -212,13 +186,13 @@ public final class Calendar { * Is the calendar selected to be displayed? * <P>Type: INTEGER (boolean)</P> */ - public static final String SELECTED = "selected"; + public static final String VISIBLE = "visible"; /** * The timezone the calendar's events occurs in * <P>Type: TEXT</P> */ - public static final String TIMEZONE = "timezone"; + public static final String CALENDAR_TIMEZONE = "calendar_timezone"; /** * If this calendar is in the list of calendars that are selected for @@ -282,35 +256,39 @@ public final class Calendar { ContentValues cv = new ContentValues(); cv.put(_ID, calendarId); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ACCOUNT); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ACCOUNT_TYPE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ACCOUNT_NAME); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ACCOUNT_TYPE); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_TIME); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA); - DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY); - DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_MARK); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DIRTY); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC1); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC2); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC3); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC4); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC5); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC6); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.NAME); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.DISPLAY_NAME); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, Calendars.COLOR); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, Calendars.CALENDAR_COLOR); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ACCESS_LEVEL); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SELECTED); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, VISIBLE); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SYNC_EVENTS); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.LOCATION); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, TIMEZONE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + Calendars.CALENDAR_LOCATION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CALENDAR_TIMEZONE); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.OWNER_ACCOUNT); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, - Calendars.ORGANIZER_CAN_RESPOND); + Calendars.CAN_ORGANIZER_RESPOND); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, + Calendars.CAN_MODIFY_TIME_ZONE); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, + Calendars.MAX_REMINDERS); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED); @@ -329,11 +307,12 @@ public final class Calendar { /** * Contains a list of available calendars. */ - public static class Calendars implements BaseColumns, SyncColumns, AccountColumns, + public static class Calendars implements BaseColumns, SyncColumns, CalendarsColumns { - private static final String WHERE_DELETE_FOR_ACCOUNT = Calendars._SYNC_ACCOUNT + "=?" - + " AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?"; + private static final String WHERE_DELETE_FOR_ACCOUNT = Calendars.ACCOUNT_NAME + "=?" + + " AND " + + Calendars.ACCOUNT_TYPE + "=?"; public static final Cursor query(ContentResolver cr, String[] projection, String where, String orderBy) @@ -418,7 +397,7 @@ public final class Calendar { * The location the of the events in the calendar * <P>Type: TEXT</P> */ - public static final String LOCATION = "location"; + public static final String CALENDAR_LOCATION = "calendar_location"; /** * The owner account for this calendar, based on the calendar feed. @@ -432,7 +411,56 @@ public final class Calendar { * organizer should not be shown by the UI. Defaults to 1 * <P>Type: INTEGER (boolean)</P> */ - public static final String ORGANIZER_CAN_RESPOND = "organizerCanRespond"; + public static final String CAN_ORGANIZER_RESPOND = "canOrganizerRespond"; + + /** + * Can the organizer modify the time zone of the event? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String CAN_MODIFY_TIME_ZONE = "canModifyTimeZone"; + + /** + * The maximum number of reminders allowed for an event. + * <P>Type: INTEGER</P> + */ + public static final String MAX_REMINDERS = "maxReminders"; + + /** + * The maximum number of reminders allowed for an event. + * <P> + * Type: INTEGER + * </P> + */ + public static final String ALLOWED_REMINDERS = "allowedReminders"; + + /** + * These fields are only writable by a sync adapter. To modify them the + * caller must include CALLER_IS_SYNCADAPTER, _SYNC_ACCOUNT, and + * _SYNC_ACCOUNT_TYPE in the query parameters. + */ + public static final String[] SYNC_WRITABLE_COLUMNS = new String[] { + ACCOUNT_NAME, + ACCOUNT_TYPE, + _SYNC_ID, + _SYNC_TIME, + _SYNC_VERSION, + DIRTY, + OWNER_ACCOUNT, + MAX_REMINDERS, + CAN_MODIFY_TIME_ZONE, + CAN_ORGANIZER_RESPOND, + CALENDAR_LOCATION, + CALENDAR_TIMEZONE, + ACCESS_LEVEL, + DELETED, + SYNC1, + SYNC2, + SYNC3, + SYNC4, + SYNC5, + SYNC6, + SYNC_STATE, + }; } /** @@ -505,6 +533,15 @@ public final class Calendar { */ public interface EventsColumns { /** + * For use by sync adapter at its discretion; not modified by CalendarProvider + * Note that this column was formerly named _SYNC_LOCAL_ID. We are using it to avoid a + * schema change. + * TODO Replace this with something more general in the future. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_DATA = "_sync_local_id"; + + /** * The calendar the event belongs to * <P>Type: INTEGER (foreign key to the Calendars table)</P> */ @@ -558,7 +595,7 @@ public final class Calendar { * This column is available for use by sync adapters * <P>Type: TEXT</P> */ - public static final String SYNC_ADAPTER_DATA = "syncAdapterData"; + public static final String SYNC_DATA1 = "sync_data1"; /** * The comments feed uri. @@ -606,7 +643,7 @@ public final class Calendar { * The timezone for the event, allDay events will have a local tz instead of UTC * <P>Type: TEXT */ - public static final String EVENT_TIMEZONE2 = "eventTimezone2"; + public static final String EVENT_END_TIMEZONE = "eventEndTimezone"; /** * Whether the event lasts all day or not @@ -615,25 +652,49 @@ public final class Calendar { public static final String ALL_DAY = "allDay"; /** - * Visibility for the event. + * Defines how the event shows up for others when the calendar is + * shared. * <P>Type: INTEGER</P> */ - public static final String VISIBILITY = "visibility"; + public static final String ACCESS_LEVEL = "accessLevel"; - public static final int VISIBILITY_DEFAULT = 0; - public static final int VISIBILITY_CONFIDENTIAL = 1; - public static final int VISIBILITY_PRIVATE = 2; - public static final int VISIBILITY_PUBLIC = 3; + /** + * Default access is controlled by the server and will be treated as + * public on the device. + */ + public static final int ACCESS_DEFAULT = 0; + /** + * Confidential is not used by the app. + */ + public static final int ACCESS_CONFIDENTIAL = 1; + /** + * Private assumes the event appears as a free/busy slot with no + * details. + */ + public static final int ACCESS_PRIVATE = 2; + /** + * Public assumes the contents are visible to anyone with access to the + * calendar. + */ + public static final int ACCESS_PUBLIC = 3; /** - * Transparency for the event -- does the event consume time on the calendar? + * If this event counts as busy time or is still free time that can be + * scheduled over. * <P>Type: INTEGER</P> */ - public static final String TRANSPARENCY = "transparency"; - - public static final int TRANSPARENCY_OPAQUE = 0; + public static final String AVAILABILITY = "availability"; - public static final int TRANSPARENCY_TRANSPARENT = 1; + /** + * Indicates that this event takes up time and will conflict with other + * events. + */ + public static final int AVAILABILITY_BUSY = 0; + /** + * Indicates that this event is free time and will not conflict with + * other events. + */ + public static final int AVAILABILITY_FREE = 1; /** * Whether the event has an alarm or not @@ -677,7 +738,7 @@ public final class Calendar { * an exception. * <P>Type: TEXT</P> */ - public static final String ORIGINAL_EVENT = "originalEvent"; + public static final String ORIGINAL_SYNC_ID = "original_sync_id"; /** * The original instance time of the recurring event for which this @@ -755,10 +816,10 @@ public final class Calendar { } /** - * Contains one entry per calendar event. Recurring events show up as a single entry. + * Contains one entry per calendar event. Recurring events show up as a + * single entry. */ - public static final class EventsEntity implements BaseColumns, SyncColumns, AccountColumns, - EventsColumns { + public static final class EventsEntity implements BaseColumns, SyncColumns, EventsColumns { /** * The content:// style URL for this table */ @@ -839,8 +900,8 @@ public final class Calendar { DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, DURATION); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_TIMEZONE); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ALL_DAY); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, VISIBILITY); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, TRANSPARENCY); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ACCESS_LEVEL); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, AVAILABILITY); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HAS_ALARM); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HAS_EXTENDED_PROPERTIES); @@ -848,7 +909,7 @@ public final class Calendar { DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, RDATE); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EXRULE); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EXDATE); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORIGINAL_EVENT); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORIGINAL_SYNC_ID); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, ORIGINAL_INSTANCE_TIME); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ORIGINAL_ALL_DAY); @@ -861,12 +922,12 @@ public final class Calendar { DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORGANIZER); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA); - DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DIRTY); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, EventsColumns.DELETED); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC1); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, - Events.SYNC_ADAPTER_DATA); + Events.SYNC_DATA1); Entity entity = new Entity(cv); Cursor subCursor; @@ -957,11 +1018,10 @@ public final class Calendar { /** * Contains one entry per calendar event. Recurring events show up as a single entry. */ - public static final class Events implements BaseColumns, SyncColumns, AccountColumns, - EventsColumns { + public static final class Events implements BaseColumns, SyncColumns, EventsColumns { private static final String[] FETCH_ENTRY_COLUMNS = - new String[] { Events._SYNC_ACCOUNT, Events._SYNC_ID }; + new String[] { Events.ACCOUNT_NAME, Events._SYNC_ID }; private static final String[] ATTENDEES_COLUMNS = new String[] { AttendeesColumns.ATTENDEE_NAME, @@ -1003,6 +1063,29 @@ public final class Calendar { * The default sort order for this table */ public static final String DEFAULT_SORT_ORDER = ""; + + /** + * These are columns that should only ever be updated by the provider, + * either because they are views mapped to another table or because they + * are used for provider only functionality. + */ + public static String[] PROVIDER_WRITABLE_COLUMNS = new String[] { + ACCOUNT_NAME, + ACCOUNT_TYPE + }; + + /** + * These fields are only writable by a sync adapter. To modify them the + * caller must include CALLER_IS_SYNCADAPTER, _SYNC_ACCOUNT, and + * _SYNC_ACCOUNT_TYPE in the query parameters. + */ + public static final String[] SYNC_WRITABLE_COLUMNS = new String[] { + _SYNC_ID, + _SYNC_TIME, + _SYNC_VERSION, + DIRTY, + SYNC_DATA1 + }; } /** @@ -1011,7 +1094,7 @@ public final class Calendar { */ public static final class Instances implements BaseColumns, EventsColumns, CalendarsColumns { - private static final String WHERE_CALENDARS_SELECTED = Calendars.SELECTED + "=1"; + private static final String WHERE_CALENDARS_SELECTED = Calendars.VISIBLE + "=1"; public static final Cursor query(ContentResolver cr, String[] projection, long begin, long end) { diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 4f88612..22b4c76 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -144,6 +144,27 @@ public final class ContactsContract { public static final String LIMIT_PARAM_KEY = "limit"; /** + * A query parameter specifing a primary account. This parameter should be used with + * {@link #PRIMARY_ACCOUNT_TYPE}. The contacts provider handling a query may rely on + * this information to optimize its query results. + * + * For example, in an email composition screen, its implementation can specify an account when + * obtaining possible recipients, letting the provider know which account is selected during + * the composition. The provider may use the "primary account" information to optimize + * the search result. + * @hide + */ + public static final String PRIMARY_ACCOUNT_NAME = "name_for_primary_account"; + + /** + * A query parameter specifing a primary account. This parameter should be used with + * {@link #PRIMARY_ACCOUNT_NAME}. See the doc in {@link #PRIMARY_ACCOUNT_NAME}. + * @hide + */ + public static final String PRIMARY_ACCOUNT_TYPE = "type_for_primary_account"; + + + /** * @hide */ public static final class Preferences { diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index c9b2f97..3bc1348 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -166,7 +166,6 @@ public final class MediaStore { * If the EXTRA_OUTPUT is present, then the full-sized image will be written to the Uri * value of EXTRA_OUTPUT. * @see #EXTRA_OUTPUT - * @see #EXTRA_VIDEO_QUALITY */ public final static String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE"; @@ -181,6 +180,9 @@ public final class MediaStore { * written to the standard location for videos, and the Uri of that location will be * returned in the data field of the Uri. * @see #EXTRA_OUTPUT + * @see #EXTRA_VIDEO_QUALITY + * @see #EXTRA_SIZE_LIMIT + * @see #EXTRA_DURATION_LIMIT */ public final static String ACTION_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE"; @@ -1094,12 +1096,6 @@ public final class MediaStore { public static final String ALBUM_KEY = "album_key"; /** - * A URI to the album art, if any - * <P>Type: TEXT</P> - */ - public static final String ALBUM_ART = "album_art"; - - /** * The track number of this song on the album, if any. * This number encodes both the track number and the * disc number. For multi-disc sets, this number will diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index b09b44e..570b801 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -41,7 +41,6 @@ import android.os.RemoteException; import android.os.SystemProperties; import android.text.TextUtils; import android.util.AndroidException; -import android.util.Config; import android.util.Log; import java.net.URISyntaxException; @@ -569,7 +568,7 @@ public final class Settings { public static final String AUTHORITY = "settings"; private static final String TAG = "Settings"; - private static final boolean LOCAL_LOGV = Config.LOGV || false; + private static final boolean LOCAL_LOGV = false || false; public static class SettingNotFoundException extends AndroidException { public SettingNotFoundException(String msg) { @@ -1103,6 +1102,18 @@ public final class Settings { public static final int END_BUTTON_BEHAVIOR_DEFAULT = END_BUTTON_BEHAVIOR_SLEEP; /** + * Is advanced settings mode turned on. 0 == no, 1 == yes + * @hide + */ + public static final String ADVANCED_SETTINGS = "advanced_settings"; + + /** + * ADVANCED_SETTINGS default value. + * @hide + */ + public static final int ADVANCED_SETTINGS_DEFAULT = 0; + + /** * Whether Airplane Mode is on. */ public static final String AIRPLANE_MODE_ON = "airplane_mode_on"; @@ -1494,6 +1505,13 @@ public final class Settings { public static final Uri DEFAULT_ALARM_ALERT_URI = getUriFor(ALARM_ALERT); /** + * Persistent store for the system default media button event receiver. + * + * @hide + */ + public static final String MEDIA_BUTTON_RECEIVER = "media_button_receiver"; + + /** * Setting to enable Auto Replace (AutoText) in text editors. 1 = On, 0 = Off */ public static final String TEXT_AUTO_REPLACE = "auto_replace"; @@ -3303,12 +3321,20 @@ public final class Settings { public static final String WIFI_IDLE_MS = "wifi_idle_ms"; /** - * The interval in milliseconds to issue scans when the driver is - * started. This is necessary to allow wifi to connect to an - * access point when the driver is suspended. + * The interval in milliseconds to issue wake up scans when wifi needs + * to connect. This is necessary to connect to an access point when + * device is on the move and the screen is off. + * @hide + */ + public static final String WIFI_FRAMEWORK_SCAN_INTERVAL_MS = + "wifi_framework_scan_interval_ms"; + + /** + * The interval in milliseconds to scan as used by the wifi supplicant * @hide */ - public static final String WIFI_SCAN_INTERVAL_MS = "wifi_scan_interval_ms"; + public static final String WIFI_SUPPLICANT_SCAN_INTERVAL_MS = + "wifi_supplicant_scan_interval_ms"; /** * The interval in milliseconds at which to check packet counts on the diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index d2d2557..91a72a5 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -28,7 +28,6 @@ import android.net.Uri; import android.os.Environment; import android.telephony.SmsMessage; import android.text.TextUtils; -import android.util.Config; import android.util.Log; import android.util.Patterns; @@ -46,7 +45,7 @@ import java.util.regex.Pattern; public final class Telephony { private static final String TAG = "Telephony"; private static final boolean DEBUG = true; - private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean LOCAL_LOGV = false; // Constructor public Telephony() { @@ -90,12 +89,18 @@ public final class Telephony { public static final String PERSON_ID = "person"; /** - * The date the message was sent + * The date the message was received * <P>Type: INTEGER (long)</P> */ public static final String DATE = "date"; /** + * The date the message was sent + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE_SENT = "date_sent"; + + /** * Has the message been read * <P>Type: INTEGER (boolean)</P> */ @@ -650,12 +655,18 @@ public final class Telephony { public static final int MESSAGE_BOX_OUTBOX = 4; /** - * The date the message was sent. + * The date the message was received. * <P>Type: INTEGER (long)</P> */ public static final String DATE = "date"; /** + * The date the message was sent. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE_SENT = "date_sent"; + + /** * The box which the message belong to, for example, MESSAGE_BOX_INBOX. * <P>Type: INTEGER</P> */ diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java index 132c346..ca2212c 100644 --- a/core/java/android/server/BluetoothA2dpService.java +++ b/core/java/android/server/BluetoothA2dpService.java @@ -83,19 +83,6 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { onBluetoothDisable(); break; } - } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { - int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, - BluetoothDevice.ERROR); - switch(bondState) { - case BluetoothDevice.BOND_BONDED: - if (getPriority(device) == BluetoothA2dp.PRIORITY_UNDEFINED) { - setPriority(device, BluetoothA2dp.PRIORITY_ON); - } - break; - case BluetoothDevice.BOND_NONE: - setPriority(device, BluetoothA2dp.PRIORITY_UNDEFINED); - break; - } } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { synchronized (this) { if (mAudioDevices.containsKey(device)) { @@ -158,7 +145,6 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { mAdapter = BluetoothAdapter.getDefaultAdapter(); mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); - mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); mIntentFilter.addAction(AudioManager.VOLUME_CHANGED_ACTION); diff --git a/core/java/android/server/BluetoothAdapterProperties.java b/core/java/android/server/BluetoothAdapterProperties.java index ae8104b..9723f60 100644 --- a/core/java/android/server/BluetoothAdapterProperties.java +++ b/core/java/android/server/BluetoothAdapterProperties.java @@ -76,14 +76,13 @@ class BluetoothAdapterProperties { for (int i = 0; i < properties.length; i++) { String name = properties[i]; String newValue = null; - int len; if (name == null) { Log.e(TAG, "Error:Adapter Property at index " + i + " is null"); continue; } if (name.equals("Devices") || name.equals("UUIDs")) { StringBuilder str = new StringBuilder(); - len = Integer.valueOf(properties[++i]); + int len = Integer.valueOf(properties[++i]); for (int j = 0; j < len; j++) { str.append(properties[++i]); str.append(","); diff --git a/core/java/android/server/BluetoothBondState.java b/core/java/android/server/BluetoothBondState.java index 2304a70..a36cd24 100644 --- a/core/java/android/server/BluetoothBondState.java +++ b/core/java/android/server/BluetoothBondState.java @@ -18,6 +18,9 @@ package android.server; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothHeadset; import android.content.Context; import android.content.Intent; import android.util.Log; @@ -68,6 +71,8 @@ class BluetoothBondState { private final Context mContext; private final BluetoothService mService; private final BluetoothInputProfileHandler mBluetoothInputProfileHandler; + private BluetoothA2dp mA2dpProxy; + private BluetoothHeadset mHeadsetProxy; BluetoothBondState(Context context, BluetoothService service) { mContext = context; @@ -126,14 +131,15 @@ class BluetoothBondState { if (state == BluetoothDevice.BOND_BONDED) { mService.addProfileState(address); + } else if (state == BluetoothDevice.BOND_BONDING) { + if (mA2dpProxy == null || mHeadsetProxy == null) { + getProfileProxy(); + } } else if (state == BluetoothDevice.BOND_NONE) { mService.removeProfileState(address); } - // HID is handled by BluetoothService, other profiles - // will be handled by their respective services. - mBluetoothInputProfileHandler.setInitialInputDevicePriority( - mService.getRemoteDevice(address), state); + setProfilePriorities(address, state); if (DBG) { Log.d(TAG, address + " bond state " + oldState + " -> " + state @@ -261,6 +267,52 @@ class BluetoothBondState { mPinAttempt.put(address, new Integer(newAttempt)); } + private void getProfileProxy() { + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (mA2dpProxy == null) { + bluetoothAdapter.getProfileProxy(mContext, mProfileServiceListener, + BluetoothProfile.A2DP); + } + + if (mHeadsetProxy == null) { + bluetoothAdapter.getProfileProxy(mContext, mProfileServiceListener, + BluetoothProfile.HEADSET); + } + } + + private void closeProfileProxy() { + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (mA2dpProxy != null) { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dpProxy); + } + + if (mHeadsetProxy != null) { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadsetProxy); + } + } + + private BluetoothProfile.ServiceListener mProfileServiceListener = + new BluetoothProfile.ServiceListener() { + + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (profile == BluetoothProfile.A2DP) { + mA2dpProxy = (BluetoothA2dp) proxy; + } else if (profile == BluetoothProfile.HEADSET) { + mHeadsetProxy = (BluetoothHeadset) proxy; + } + } + + public void onServiceDisconnected(int profile) { + if (profile == BluetoothProfile.A2DP) { + mA2dpProxy = null; + } else if (profile == BluetoothProfile.HEADSET) { + mHeadsetProxy = null; + } + } + }; + private void copyAutoPairingData() { FileInputStream in = null; FileOutputStream out = null; @@ -365,4 +417,30 @@ class BluetoothBondState { } } } + + // Set service priority of Hid, A2DP and Headset profiles depending on + // the bond state change + private void setProfilePriorities(String address, int state) { + BluetoothDevice remoteDevice = mService.getRemoteDevice(address); + // HID is handled by BluetoothService + mBluetoothInputProfileHandler.setInitialInputDevicePriority(remoteDevice, state); + + // Set service priority of A2DP and Headset + // We used to do the priority change in the 2 services after the broadcast + // intent reach them. But that left a small time gap that could reject + // incoming connection due to undefined priorities. + if (state == BluetoothDevice.BOND_BONDED) { + if (mA2dpProxy.getPriority(remoteDevice) == BluetoothProfile.PRIORITY_UNDEFINED) { + mA2dpProxy.setPriority(remoteDevice, BluetoothProfile.PRIORITY_ON); + } + + if (mHeadsetProxy.getPriority(remoteDevice) == BluetoothProfile.PRIORITY_UNDEFINED) { + mHeadsetProxy.setPriority(remoteDevice, BluetoothProfile.PRIORITY_ON); + } + } else if (state == BluetoothDevice.BOND_NONE) { + mA2dpProxy.setPriority(remoteDevice, BluetoothProfile.PRIORITY_UNDEFINED); + mHeadsetProxy.setPriority(remoteDevice, BluetoothProfile.PRIORITY_UNDEFINED); + } + } + } diff --git a/core/java/android/server/BluetoothInputProfileHandler.java b/core/java/android/server/BluetoothInputProfileHandler.java index e6513fd..247e297 100644 --- a/core/java/android/server/BluetoothInputProfileHandler.java +++ b/core/java/android/server/BluetoothInputProfileHandler.java @@ -60,7 +60,7 @@ final class BluetoothInputProfileHandler { return sInstance; } - synchronized boolean connectInputDevice(BluetoothDevice device, + boolean connectInputDevice(BluetoothDevice device, BluetoothDeviceProfileState state) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); if (objectPath == null || @@ -78,7 +78,7 @@ final class BluetoothInputProfileHandler { return false; } - synchronized boolean connectInputDeviceInternal(BluetoothDevice device) { + boolean connectInputDeviceInternal(BluetoothDevice device) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_CONNECTING); if (!mBluetoothService.connectInputDeviceNative(objectPath)) { @@ -88,7 +88,7 @@ final class BluetoothInputProfileHandler { return true; } - synchronized boolean disconnectInputDevice(BluetoothDevice device, + boolean disconnectInputDevice(BluetoothDevice device, BluetoothDeviceProfileState state) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); if (objectPath == null || @@ -105,7 +105,7 @@ final class BluetoothInputProfileHandler { return false; } - synchronized boolean disconnectInputDeviceInternal(BluetoothDevice device) { + boolean disconnectInputDeviceInternal(BluetoothDevice device) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_DISCONNECTING); if (!mBluetoothService.disconnectInputDeviceNative(objectPath)) { @@ -115,31 +115,31 @@ final class BluetoothInputProfileHandler { return true; } - synchronized int getInputDeviceConnectionState(BluetoothDevice device) { + int getInputDeviceConnectionState(BluetoothDevice device) { if (mInputDevices.get(device) == null) { return BluetoothInputDevice.STATE_DISCONNECTED; } return mInputDevices.get(device); } - synchronized List<BluetoothDevice> getConnectedInputDevices() { + List<BluetoothDevice> getConnectedInputDevices() { List<BluetoothDevice> devices = lookupInputDevicesMatchingStates( new int[] {BluetoothInputDevice.STATE_CONNECTED}); return devices; } - synchronized List<BluetoothDevice> getInputDevicesMatchingConnectionStates(int[] states) { + List<BluetoothDevice> getInputDevicesMatchingConnectionStates(int[] states) { List<BluetoothDevice> devices = lookupInputDevicesMatchingStates(states); return devices; } - synchronized int getInputDevicePriority(BluetoothDevice device) { + int getInputDevicePriority(BluetoothDevice device) { return Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.getBluetoothInputDevicePriorityKey(device.getAddress()), BluetoothInputDevice.PRIORITY_UNDEFINED); } - synchronized boolean setInputDevicePriority(BluetoothDevice device, int priority) { + boolean setInputDevicePriority(BluetoothDevice device, int priority) { if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { return false; } @@ -148,7 +148,7 @@ final class BluetoothInputProfileHandler { priority); } - synchronized List<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) { + List<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) { List<BluetoothDevice> inputDevices = new ArrayList<BluetoothDevice>(); for (BluetoothDevice device: mInputDevices.keySet()) { @@ -163,7 +163,7 @@ final class BluetoothInputProfileHandler { return inputDevices; } - private synchronized void handleInputDeviceStateChange(BluetoothDevice device, int state) { + private void handleInputDeviceStateChange(BluetoothDevice device, int state) { int prevState; if (mInputDevices.get(device) == null) { prevState = BluetoothInputDevice.STATE_DISCONNECTED; @@ -194,7 +194,7 @@ final class BluetoothInputProfileHandler { mBluetoothService.sendConnectionStateChange(device, state, prevState); } - synchronized void handleInputDevicePropertyChange(String address, boolean connected) { + void handleInputDevicePropertyChange(String address, boolean connected) { int state = connected ? BluetoothInputDevice.STATE_CONNECTED : BluetoothInputDevice.STATE_DISCONNECTED; BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); @@ -202,7 +202,7 @@ final class BluetoothInputProfileHandler { handleInputDeviceStateChange(device, state); } - synchronized void setInitialInputDevicePriority(BluetoothDevice device, int state) { + void setInitialInputDevicePriority(BluetoothDevice device, int state) { switch (state) { case BluetoothDevice.BOND_BONDED: if (getInputDevicePriority(device) == BluetoothInputDevice.PRIORITY_UNDEFINED) { diff --git a/core/java/android/server/BluetoothPanProfileHandler.java b/core/java/android/server/BluetoothPanProfileHandler.java index 8925856..0d63e19 100644 --- a/core/java/android/server/BluetoothPanProfileHandler.java +++ b/core/java/android/server/BluetoothPanProfileHandler.java @@ -76,17 +76,17 @@ final class BluetoothPanProfileHandler { } } - static synchronized BluetoothPanProfileHandler getInstance(Context context, + static BluetoothPanProfileHandler getInstance(Context context, BluetoothService service) { if (sInstance == null) sInstance = new BluetoothPanProfileHandler(context, service); return sInstance; } - synchronized boolean isTetheringOn() { + boolean isTetheringOn() { return mTetheringOn; } - synchronized boolean allowIncomingTethering() { + boolean allowIncomingTethering() { if (isTetheringOn() && getConnectedPanDevices().size() < mMaxPanDevices) return true; return false; @@ -94,7 +94,7 @@ final class BluetoothPanProfileHandler { private BroadcastReceiver mTetheringReceiver = null; - synchronized void setBluetoothTethering(boolean value) { + void setBluetoothTethering(boolean value) { if (!value) { disconnectPanServerDevices(); } @@ -104,7 +104,7 @@ final class BluetoothPanProfileHandler { filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); mTetheringReceiver = new BroadcastReceiver() { @Override - public synchronized void onReceive(Context context, Intent intent) { + public void onReceive(Context context, Intent intent) { if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF) == BluetoothAdapter.STATE_ON) { mTetheringOn = true; @@ -118,7 +118,7 @@ final class BluetoothPanProfileHandler { } } - synchronized int getPanDeviceConnectionState(BluetoothDevice device) { + int getPanDeviceConnectionState(BluetoothDevice device) { BluetoothPanDevice panDevice = mPanDevices.get(device); if (panDevice == null) { return BluetoothPan.STATE_DISCONNECTED; @@ -126,7 +126,7 @@ final class BluetoothPanProfileHandler { return panDevice.mState; } - synchronized boolean connectPanDevice(BluetoothDevice device) { + boolean connectPanDevice(BluetoothDevice device) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); if (DBG) Log.d(TAG, "connect PAN(" + objectPath + ")"); if (getPanDeviceConnectionState(device) != BluetoothPan.STATE_DISCONNECTED) { @@ -158,7 +158,7 @@ final class BluetoothPanProfileHandler { } } - private synchronized boolean disconnectPanServerDevices() { + private boolean disconnectPanServerDevices() { debugLog("disconnect all PAN devices"); for (BluetoothDevice device: mPanDevices.keySet()) { @@ -187,7 +187,7 @@ final class BluetoothPanProfileHandler { return true; } - synchronized List<BluetoothDevice> getConnectedPanDevices() { + List<BluetoothDevice> getConnectedPanDevices() { List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>(); for (BluetoothDevice device: mPanDevices.keySet()) { @@ -198,7 +198,7 @@ final class BluetoothPanProfileHandler { return devices; } - synchronized List<BluetoothDevice> getPanDevicesMatchingConnectionStates(int[] states) { + List<BluetoothDevice> getPanDevicesMatchingConnectionStates(int[] states) { List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>(); for (BluetoothDevice device: mPanDevices.keySet()) { @@ -213,7 +213,7 @@ final class BluetoothPanProfileHandler { return devices; } - synchronized boolean disconnectPanDevice(BluetoothDevice device) { + boolean disconnectPanDevice(BluetoothDevice device) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); debugLog("disconnect PAN(" + objectPath + ")"); @@ -249,7 +249,7 @@ final class BluetoothPanProfileHandler { return true; } - synchronized void handlePanDeviceStateChange(BluetoothDevice device, + void handlePanDeviceStateChange(BluetoothDevice device, String iface, int state, int role) { int prevState; String ifaceAddr = null; @@ -304,7 +304,7 @@ final class BluetoothPanProfileHandler { mBluetoothService.sendConnectionStateChange(device, state, prevState); } - synchronized void handlePanDeviceStateChange(BluetoothDevice device, + void handlePanDeviceStateChange(BluetoothDevice device, int state, int role) { handlePanDeviceStateChange(device, null, state, role); } @@ -343,7 +343,7 @@ final class BluetoothPanProfileHandler { } // configured when we start tethering - private synchronized String enableTethering(String iface) { + private String enableTethering(String iface) { debugLog("updateTetherState:" + iface); IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index f98d275..2c79385 100644 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -1928,120 +1928,163 @@ public class BluetoothService extends IBluetooth.Stub { } /**** Handlers for PAN Profile ****/ + // TODO: This needs to be converted to a state machine. - public synchronized boolean isTetheringOn() { + public boolean isTetheringOn() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothPanProfileHandler.isTetheringOn(); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.isTetheringOn(); + } } - /*package*/ synchronized boolean allowIncomingTethering() { - return mBluetoothPanProfileHandler.allowIncomingTethering(); + /*package*/boolean allowIncomingTethering() { + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.allowIncomingTethering(); + } } - public synchronized void setBluetoothTethering(boolean value) { + public void setBluetoothTethering(boolean value) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - mBluetoothPanProfileHandler.setBluetoothTethering(value); + synchronized (mBluetoothPanProfileHandler) { + mBluetoothPanProfileHandler.setBluetoothTethering(value); + } } - public synchronized int getPanDeviceConnectionState(BluetoothDevice device) { + public int getPanDeviceConnectionState(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothPanProfileHandler.getPanDeviceConnectionState(device); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.getPanDeviceConnectionState(device); + } } - public synchronized boolean connectPanDevice(BluetoothDevice device) { + public boolean connectPanDevice(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - return mBluetoothPanProfileHandler.connectPanDevice(device); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.connectPanDevice(device); + } } - public synchronized List<BluetoothDevice> getConnectedPanDevices() { + public List<BluetoothDevice> getConnectedPanDevices() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothPanProfileHandler.getConnectedPanDevices(); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.getConnectedPanDevices(); + } } - public synchronized List<BluetoothDevice> getPanDevicesMatchingConnectionStates( + public List<BluetoothDevice> getPanDevicesMatchingConnectionStates( int[] states) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothPanProfileHandler.getPanDevicesMatchingConnectionStates(states); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.getPanDevicesMatchingConnectionStates(states); + } } - public synchronized boolean disconnectPanDevice(BluetoothDevice device) { + public boolean disconnectPanDevice(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - return mBluetoothPanProfileHandler.disconnectPanDevice(device); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.disconnectPanDevice(device); + } } - /*package*/ synchronized void handlePanDeviceStateChange(BluetoothDevice device, + /*package*/void handlePanDeviceStateChange(BluetoothDevice device, String iface, int state, int role) { - mBluetoothPanProfileHandler.handlePanDeviceStateChange(device, iface, state, role); + synchronized (mBluetoothPanProfileHandler) { + mBluetoothPanProfileHandler.handlePanDeviceStateChange(device, iface, state, role); + } } - /*package*/ synchronized void handlePanDeviceStateChange(BluetoothDevice device, + /*package*/void handlePanDeviceStateChange(BluetoothDevice device, int state, int role) { - mBluetoothPanProfileHandler.handlePanDeviceStateChange(device, null, state, role); + synchronized (mBluetoothPanProfileHandler) { + mBluetoothPanProfileHandler.handlePanDeviceStateChange(device, null, state, role); + } } /**** Handlers for Input Device Profile ****/ + // This needs to be converted to state machine - public synchronized boolean connectInputDevice(BluetoothDevice device) { + public boolean connectInputDevice(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); BluetoothDeviceProfileState state = mDeviceProfileState.get(device.getAddress()); - return mBluetoothInputProfileHandler.connectInputDevice(device, state); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.connectInputDevice(device, state); + } } - public synchronized boolean connectInputDeviceInternal(BluetoothDevice device) { - return mBluetoothInputProfileHandler.connectInputDeviceInternal(device); + public boolean connectInputDeviceInternal(BluetoothDevice device) { + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.connectInputDeviceInternal(device); + } } - public synchronized boolean disconnectInputDevice(BluetoothDevice device) { + public boolean disconnectInputDevice(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); BluetoothDeviceProfileState state = mDeviceProfileState.get(device.getAddress()); - return mBluetoothInputProfileHandler.disconnectInputDevice(device, state); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.disconnectInputDevice(device, state); + } } - public synchronized boolean disconnectInputDeviceInternal(BluetoothDevice device) { - return mBluetoothInputProfileHandler.disconnectInputDeviceInternal(device); + public boolean disconnectInputDeviceInternal(BluetoothDevice device) { + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.disconnectInputDeviceInternal(device); + } } - public synchronized int getInputDeviceConnectionState(BluetoothDevice device) { + public int getInputDeviceConnectionState(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothInputProfileHandler.getInputDeviceConnectionState(device); - + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.getInputDeviceConnectionState(device); + } } - public synchronized List<BluetoothDevice> getConnectedInputDevices() { + public List<BluetoothDevice> getConnectedInputDevices() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothInputProfileHandler.getConnectedInputDevices(); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.getConnectedInputDevices(); + } } - public synchronized List<BluetoothDevice> getInputDevicesMatchingConnectionStates( + public List<BluetoothDevice> getInputDevicesMatchingConnectionStates( int[] states) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothInputProfileHandler.getInputDevicesMatchingConnectionStates(states); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.getInputDevicesMatchingConnectionStates(states); + } } - public synchronized int getInputDevicePriority(BluetoothDevice device) { + public int getInputDevicePriority(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothInputProfileHandler.getInputDevicePriority(device); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.getInputDevicePriority(device); + } } - public synchronized boolean setInputDevicePriority(BluetoothDevice device, int priority) { + public boolean setInputDevicePriority(BluetoothDevice device, int priority) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - return mBluetoothInputProfileHandler.setInputDevicePriority(device, priority); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.setInputDevicePriority(device, priority); + } } - /*package*/synchronized List<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) { - return mBluetoothInputProfileHandler.lookupInputDevicesMatchingStates(states); + /*package*/List<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) { + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.lookupInputDevicesMatchingStates(states); + } } - /*package*/ synchronized void handleInputDevicePropertyChange(String address, boolean connected) { - mBluetoothInputProfileHandler.handleInputDevicePropertyChange(address, connected); + /*package*/void handleInputDevicePropertyChange(String address, boolean connected) { + synchronized (mBluetoothInputProfileHandler) { + mBluetoothInputProfileHandler.handleInputDevicePropertyChange(address, connected); + } } public boolean connectHeadset(String address) { diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 20661d7..eae7574 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -51,7 +51,7 @@ import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.View; import android.view.ViewGroup; -import android.view.ViewRoot; +import android.view.ViewAncestor; import android.view.WindowManager; import android.view.WindowManagerImpl; import android.view.WindowManagerPolicy; @@ -650,7 +650,7 @@ public abstract class WallpaperService extends Service { mWindowToken = wrapper.mWindowToken; mSurfaceHolder.setSizeFromLayout(); mInitializing = true; - mSession = ViewRoot.getWindowSession(getMainLooper()); + mSession = ViewAncestor.getWindowSession(getMainLooper()); mWindow.setSession(mSession); diff --git a/core/java/android/speech/RecognitionListener.java b/core/java/android/speech/RecognitionListener.java index 5eb71d7..bdb3ba9 100644 --- a/core/java/android/speech/RecognitionListener.java +++ b/core/java/android/speech/RecognitionListener.java @@ -70,7 +70,8 @@ public interface RecognitionListener { * * @param results the recognition results. To retrieve the results in {@code * ArrayList<String>} format use {@link Bundle#getStringArrayList(String)} with - * {@link SpeechRecognizer#RESULTS_RECOGNITION} as a parameter + * {@link SpeechRecognizer#RESULTS_RECOGNITION} as a parameter. A float array of + * confidence values might also be given in {@link SpeechRecognizer#CONFIDENCE_SCORES}. */ void onResults(Bundle results); diff --git a/core/java/android/speech/RecognizerIntent.java b/core/java/android/speech/RecognizerIntent.java index 02c324c..fd709f2 100644 --- a/core/java/android/speech/RecognizerIntent.java +++ b/core/java/android/speech/RecognizerIntent.java @@ -46,7 +46,7 @@ public class RecognizerIntent { } /** - * Starts an activity that will prompt the user for speech and sends it through a + * Starts an activity that will prompt the user for speech and send it through a * speech recognizer. The results will be returned via activity results (in * {@link Activity#onActivityResult}, if you start the intent using * {@link Activity#startActivityForResult(Intent, int)}), or forwarded via a PendingIntent @@ -81,8 +81,8 @@ public class RecognizerIntent { public static final String ACTION_RECOGNIZE_SPEECH = "android.speech.action.RECOGNIZE_SPEECH"; /** - * Starts an activity that will prompt the user for speech, sends it through a - * speech recognizer, and invokes and either displays a web search result or triggers + * Starts an activity that will prompt the user for speech, send it through a + * speech recognizer, and either display a web search result or trigger * another type of action based on the user's speech. * * <p>If you want to avoid triggering any type of action besides web search, you can use @@ -100,11 +100,13 @@ public class RecognizerIntent { * <li>{@link #EXTRA_MAX_RESULTS} * <li>{@link #EXTRA_PARTIAL_RESULTS} * <li>{@link #EXTRA_WEB_SEARCH_ONLY} + * <li>{@link #EXTRA_ORIGIN} * </ul> * * <p> Result extras (returned in the result, not to be specified in the request): * <ul> * <li>{@link #EXTRA_RESULTS} + * <li>{@link #EXTRA_CONFIDENCE_SCORES} (optional) * </ul> * * <p>NOTE: There may not be any applications installed to handle this action, so you should @@ -181,6 +183,13 @@ public class RecognizerIntent { * {@link java.util.Locale#getDefault()}. */ public static final String EXTRA_LANGUAGE = "android.speech.extra.LANGUAGE"; + + /** + * Optional value which can be used to indicate the referer url of a page in which + * speech was requested. For example, a web browser may choose to provide this for + * uses of speech on a given page. + */ + public static final String EXTRA_ORIGIN = "android.speech.extra.ORIGIN"; /** * Optional limit on the maximum number of results to return. If omitted the recognizer @@ -232,13 +241,31 @@ public class RecognizerIntent { /** * An ArrayList<String> of the recognition results when performing - * {@link #ACTION_RECOGNIZE_SPEECH}. Returned in the results; not to be specified in the - * recognition request. Only present when {@link Activity#RESULT_OK} is returned in - * an activity result. In a PendingIntent, the lack of this extra indicates failure. + * {@link #ACTION_RECOGNIZE_SPEECH}. Generally this list should be ordered in + * descending order of speech recognizer confidence. (See {@link #EXTRA_CONFIDENCE_SCORES}). + * Returned in the results; not to be specified in the recognition request. Only present + * when {@link Activity#RESULT_OK} is returned in an activity result. In a PendingIntent, + * the lack of this extra indicates failure. */ public static final String EXTRA_RESULTS = "android.speech.extra.RESULTS"; /** + * A float array of confidence scores of the recognition results when performing + * {@link #ACTION_RECOGNIZE_SPEECH}. The array should be the same size as the ArrayList + * returned in {@link #EXTRA_RESULTS}, and should contain values ranging from 0.0 to 1.0, + * or -1 to represent an unavailable confidence score. + * <p> + * Confidence values close to 1.0 indicate high confidence (the speech recognizer is + * confident that the recognition result is correct), while values close to 0.0 indicate + * low confidence. + * <p> + * Returned in the results; not to be specified in the recognition request. This extra is + * optional and might not be provided. Only present when {@link Activity#RESULT_OK} is + * returned in an activity result. + */ + public static final String EXTRA_CONFIDENCE_SCORES = "android.speech.extra.CONFIDENCE_SCORES"; + + /** * Returns the broadcast intent to fire with * {@link Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, int, String, Bundle)} * to receive details from the package that implements voice search. diff --git a/core/java/android/speech/SpeechRecognizer.java b/core/java/android/speech/SpeechRecognizer.java index cd73ba8..8fee41d 100644 --- a/core/java/android/speech/SpeechRecognizer.java +++ b/core/java/android/speech/SpeechRecognizer.java @@ -50,12 +50,26 @@ public class SpeechRecognizer { private static final String TAG = "SpeechRecognizer"; /** - * Used to retrieve an {@code ArrayList<String>} from the {@link Bundle} passed to the + * Key used to retrieve an {@code ArrayList<String>} from the {@link Bundle} passed to the * {@link RecognitionListener#onResults(Bundle)} and * {@link RecognitionListener#onPartialResults(Bundle)} methods. These strings are the possible * recognition results, where the first element is the most likely candidate. */ public static final String RESULTS_RECOGNITION = "results_recognition"; + + /** + * Key used to retrieve a float array from the {@link Bundle} passed to the + * {@link RecognitionListener#onResults(Bundle)} and + * {@link RecognitionListener#onPartialResults(Bundle)} methods. The array should be + * the same size as the ArrayList provided in {@link #RESULTS_RECOGNITION}, and should contain + * values ranging from 0.0 to 1.0, or -1 to represent an unavailable confidence score. + * <p> + * Confidence values close to 1.0 indicate high confidence (the speech recognizer is confident + * that the recognition result is correct), while values close to 0.0 indicate low confidence. + * <p> + * This value is optional and might not be provided. + */ + public static final String CONFIDENCE_SCORES = "confidence_scores"; /** Network operation timed out. */ public static final int ERROR_NETWORK_TIMEOUT = 1; diff --git a/core/java/android/speech/srec/Recognizer.java b/core/java/android/speech/srec/Recognizer.java index a03a36a..8a2bc7d 100644 --- a/core/java/android/speech/srec/Recognizer.java +++ b/core/java/android/speech/srec/Recognizer.java @@ -22,7 +22,6 @@ package android.speech.srec; -import android.util.Config; import android.util.Log; import java.io.File; diff --git a/core/java/android/speech/tts/BlockingMediaPlayer.java b/core/java/android/speech/tts/BlockingMediaPlayer.java new file mode 100644 index 0000000..3cf60dd --- /dev/null +++ b/core/java/android/speech/tts/BlockingMediaPlayer.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.content.Context; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.util.Log; + +/** + * A media player that allows blocking to wait for it to finish. + */ +class BlockingMediaPlayer { + + private static final String TAG = "BlockMediaPlayer"; + + private static final String MEDIA_PLAYER_THREAD_NAME = "TTS-MediaPlayer"; + + private final Context mContext; + private final Uri mUri; + private final int mStreamType; + private final ConditionVariable mDone; + // Only accessed on the Handler thread + private MediaPlayer mPlayer; + private volatile boolean mFinished; + + /** + * Creates a new blocking media player. + * Creating a blocking media player is a cheap operation. + * + * @param context + * @param uri + * @param streamType + */ + public BlockingMediaPlayer(Context context, Uri uri, int streamType) { + mContext = context; + mUri = uri; + mStreamType = streamType; + mDone = new ConditionVariable(); + + } + + /** + * Starts playback and waits for it to finish. + * Can be called from any thread. + * + * @return {@code true} if the playback finished normally, {@code false} if the playback + * failed or {@link #stop} was called before the playback finished. + */ + public boolean startAndWait() { + HandlerThread thread = new HandlerThread(MEDIA_PLAYER_THREAD_NAME); + thread.start(); + Handler handler = new Handler(thread.getLooper()); + mFinished = false; + handler.post(new Runnable() { + @Override + public void run() { + startPlaying(); + } + }); + mDone.block(); + handler.post(new Runnable() { + @Override + public void run() { + finish(); + // No new messages should get posted to the handler thread after this + Looper.myLooper().quit(); + } + }); + return mFinished; + } + + /** + * Stops playback. Can be called multiple times. + * Can be called from any thread. + */ + public void stop() { + mDone.open(); + } + + /** + * Starts playback. + * Called on the handler thread. + */ + private void startPlaying() { + mPlayer = MediaPlayer.create(mContext, mUri); + if (mPlayer == null) { + Log.w(TAG, "Failed to play " + mUri); + mDone.open(); + return; + } + try { + mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + Log.w(TAG, "Audio playback error: " + what + ", " + extra); + mDone.open(); + return true; + } + }); + mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mp) { + mFinished = true; + mDone.open(); + } + }); + mPlayer.setAudioStreamType(mStreamType); + mPlayer.start(); + } catch (IllegalArgumentException ex) { + Log.w(TAG, "MediaPlayer failed", ex); + mDone.open(); + } + } + + /** + * Stops playback and release the media player. + * Called on the handler thread. + */ + private void finish() { + try { + mPlayer.stop(); + } catch (IllegalStateException ex) { + // Do nothing, the player is already stopped + } + mPlayer.release(); + } + +}
\ No newline at end of file diff --git a/core/java/android/speech/tts/FileSynthesisRequest.java b/core/java/android/speech/tts/FileSynthesisRequest.java new file mode 100644 index 0000000..62be2bf --- /dev/null +++ b/core/java/android/speech/tts/FileSynthesisRequest.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.media.AudioFormat; +import android.os.Bundle; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Speech synthesis request that writes the audio to a WAV file. + */ +class FileSynthesisRequest extends SynthesisRequest { + + private static final String TAG = "FileSynthesisRequest"; + private static final boolean DBG = false; + + private static final int MAX_AUDIO_BUFFER_SIZE = 8192; + + private static final int WAV_HEADER_LENGTH = 44; + private static final short WAV_FORMAT_PCM = 0x0001; + + private final Object mStateLock = new Object(); + private final File mFileName; + private int mSampleRateInHz; + private int mAudioFormat; + private int mChannelCount; + private RandomAccessFile mFile; + private boolean mStopped = false; + private boolean mDone = false; + + FileSynthesisRequest(String text, Bundle params, File fileName) { + super(text, params); + mFileName = fileName; + } + + @Override + void stop() { + synchronized (mStateLock) { + mStopped = true; + cleanUp(); + } + } + + /** + * Must be called while holding the monitor on {@link #mStateLock}. + */ + private void cleanUp() { + closeFile(); + if (mFile != null) { + mFileName.delete(); + } + } + + /** + * Must be called while holding the monitor on {@link #mStateLock}. + */ + private void closeFile() { + try { + if (mFile != null) { + mFile.close(); + mFile = null; + } + } catch (IOException ex) { + Log.e(TAG, "Failed to close " + mFileName + ": " + ex); + } + } + + @Override + public int getMaxBufferSize() { + return MAX_AUDIO_BUFFER_SIZE; + } + + @Override + boolean isDone() { + return mDone; + } + + @Override + public int start(int sampleRateInHz, int audioFormat, int channelCount) { + if (DBG) { + Log.d(TAG, "FileSynthesisRequest.start(" + sampleRateInHz + "," + audioFormat + + "," + channelCount + ")"); + } + synchronized (mStateLock) { + if (mStopped) { + if (DBG) Log.d(TAG, "Request has been aborted."); + return TextToSpeech.ERROR; + } + if (mFile != null) { + cleanUp(); + throw new IllegalArgumentException("FileSynthesisRequest.start() called twice"); + } + mSampleRateInHz = sampleRateInHz; + mAudioFormat = audioFormat; + mChannelCount = channelCount; + try { + mFile = new RandomAccessFile(mFileName, "rw"); + // Reserve space for WAV header + mFile.write(new byte[WAV_HEADER_LENGTH]); + return TextToSpeech.SUCCESS; + } catch (IOException ex) { + Log.e(TAG, "Failed to open " + mFileName + ": " + ex); + cleanUp(); + return TextToSpeech.ERROR; + } + } + } + + @Override + public int audioAvailable(byte[] buffer, int offset, int length) { + if (DBG) { + Log.d(TAG, "FileSynthesisRequest.audioAvailable(" + buffer + "," + offset + + "," + length + ")"); + } + synchronized (mStateLock) { + if (mStopped) { + if (DBG) Log.d(TAG, "Request has been aborted."); + return TextToSpeech.ERROR; + } + if (mFile == null) { + Log.e(TAG, "File not open"); + return TextToSpeech.ERROR; + } + try { + mFile.write(buffer, offset, length); + return TextToSpeech.SUCCESS; + } catch (IOException ex) { + Log.e(TAG, "Failed to write to " + mFileName + ": " + ex); + cleanUp(); + return TextToSpeech.ERROR; + } + } + } + + @Override + public int done() { + if (DBG) Log.d(TAG, "FileSynthesisRequest.done()"); + synchronized (mStateLock) { + if (mStopped) { + if (DBG) Log.d(TAG, "Request has been aborted."); + return TextToSpeech.ERROR; + } + if (mFile == null) { + Log.e(TAG, "File not open"); + return TextToSpeech.ERROR; + } + try { + // Write WAV header at start of file + mFile.seek(0); + int dataLength = (int) (mFile.length() - WAV_HEADER_LENGTH); + mFile.write( + makeWavHeader(mSampleRateInHz, mAudioFormat, mChannelCount, dataLength)); + closeFile(); + mDone = true; + return TextToSpeech.SUCCESS; + } catch (IOException ex) { + Log.e(TAG, "Failed to write to " + mFileName + ": " + ex); + cleanUp(); + return TextToSpeech.ERROR; + } + } + } + + @Override + public void error() { + if (DBG) Log.d(TAG, "FileSynthesisRequest.error()"); + synchronized (mStateLock) { + cleanUp(); + } + } + + @Override + public int completeAudioAvailable(int sampleRateInHz, int audioFormat, int channelCount, + byte[] buffer, int offset, int length) { + synchronized (mStateLock) { + if (mStopped) { + if (DBG) Log.d(TAG, "Request has been aborted."); + return TextToSpeech.ERROR; + } + } + FileOutputStream out = null; + try { + out = new FileOutputStream(mFileName); + out.write(makeWavHeader(sampleRateInHz, audioFormat, channelCount, length)); + out.write(buffer, offset, length); + mDone = true; + return TextToSpeech.SUCCESS; + } catch (IOException ex) { + Log.e(TAG, "Failed to write to " + mFileName + ": " + ex); + mFileName.delete(); + return TextToSpeech.ERROR; + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException ex) { + Log.e(TAG, "Failed to close " + mFileName + ": " + ex); + } + } + } + + private byte[] makeWavHeader(int sampleRateInHz, int audioFormat, int channelCount, + int dataLength) { + // TODO: is AudioFormat.ENCODING_DEFAULT always the same as ENCODING_PCM_16BIT? + int sampleSizeInBytes = (audioFormat == AudioFormat.ENCODING_PCM_8BIT ? 1 : 2); + int byteRate = sampleRateInHz * sampleSizeInBytes * channelCount; + short blockAlign = (short) (sampleSizeInBytes * channelCount); + short bitsPerSample = (short) (sampleSizeInBytes * 8); + + byte[] headerBuf = new byte[WAV_HEADER_LENGTH]; + ByteBuffer header = ByteBuffer.wrap(headerBuf); + header.order(ByteOrder.LITTLE_ENDIAN); + + header.put(new byte[]{ 'R', 'I', 'F', 'F' }); + header.putInt(dataLength + WAV_HEADER_LENGTH - 8); // RIFF chunk size + header.put(new byte[]{ 'W', 'A', 'V', 'E' }); + header.put(new byte[]{ 'f', 'm', 't', ' ' }); + header.putInt(16); // size of fmt chunk + header.putShort(WAV_FORMAT_PCM); + header.putShort((short) channelCount); + header.putInt(sampleRateInHz); + header.putInt(byteRate); + header.putShort(blockAlign); + header.putShort(bitsPerSample); + header.put(new byte[]{ 'd', 'a', 't', 'a' }); + header.putInt(dataLength); + + return headerBuf; + } + +} diff --git a/core/java/android/speech/tts/ITtsCallback.aidl b/core/java/android/speech/tts/ITextToSpeechCallback.aidl index c9898eb..40902ae 100755 --- a/core/java/android/speech/tts/ITtsCallback.aidl +++ b/core/java/android/speech/tts/ITextToSpeechCallback.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 The Android Open Source Project + * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,15 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package android.speech.tts; /** - * AIDL for the callback from the TTS Service - * ITtsCallback.java is autogenerated from this. + * Interface for callbacks from TextToSpeechService * * {@hide} */ -oneway interface ITtsCallback { +oneway interface ITextToSpeechCallback { void utteranceCompleted(String utteranceId); } diff --git a/core/java/android/speech/tts/ITextToSpeechService.aidl b/core/java/android/speech/tts/ITextToSpeechService.aidl new file mode 100644 index 0000000..ff3fa11 --- /dev/null +++ b/core/java/android/speech/tts/ITextToSpeechService.aidl @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.speech.tts; + +import android.net.Uri; +import android.os.Bundle; +import android.speech.tts.ITextToSpeechCallback; + +/** + * Interface for TextToSpeech to talk to TextToSpeechService. + * + * {@hide} + */ +interface ITextToSpeechService { + + /** + * Tells the engine to synthesize some speech and play it back. + * + * @param callingApp The package name of the calling app. Used to connect requests + * callbacks and to clear requests when the calling app is stopping. + * @param text The text to synthesize. + * @param queueMode Determines what to do to requests already in the queue. + * @param param Request parameters. + */ + int speak(in String callingApp, in String text, in int queueMode, in Bundle params); + + /** + * Tells the engine to synthesize some speech and write it to a file. + * + * @param callingApp The package name of the calling app. Used to connect requests + * callbacks and to clear requests when the calling app is stopping. + * @param text The text to synthesize. + * @param filename The file to write the synthesized audio to. + * @param param Request parameters. + */ + int synthesizeToFile(in String callingApp, in String text, + in String filename, in Bundle params); + + /** + * Plays an existing audio resource. + * + * @param callingApp The package name of the calling app. Used to connect requests + * callbacks and to clear requests when the calling app is stopping. + * @param audioUri URI for the audio resource (a file or android.resource URI) + * @param queueMode Determines what to do to requests already in the queue. + * @param param Request parameters. + */ + int playAudio(in String callingApp, in Uri audioUri, in int queueMode, in Bundle params); + + /** + * Plays silence. + * + * @param callingApp The package name of the calling app. Used to connect requests + * callbacks and to clear requests when the calling app is stopping. + * @param duration Number of milliseconds of silence to play. + * @param queueMode Determines what to do to requests already in the queue. + * @param param Request parameters. + */ + int playSilence(in String callingApp, in long duration, in int queueMode, in Bundle params); + + /** + * Checks whether the service is currently playing some audio. + */ + boolean isSpeaking(); + + /** + * Interrupts the current utterance (if from the given app) and removes any utterances + * in the queue that are from the given app. + * + * @param callingApp Package name of the app whose utterances + * should be interrupted and cleared. + */ + int stop(in String callingApp); + + /** + * Returns the language, country and variant currently being used by the TTS engine. + * + * Can be called from multiple threads. + * + * @return A 3-element array, containing language (ISO 3-letter code), + * country (ISO 3-letter code) and variant used by the engine. + * The country and variant may be {@code ""}. If country is empty, then variant must + * be empty too. + */ + String[] getLanguage(); + + /** + * Checks whether the engine supports a given language. + * + * @param lang ISO-3 language code. + * @param country ISO-3 country code. May be empty or null. + * @param variant Language variant. May be empty or null. + * @return Code indicating the support status for the locale. + * One of {@link TextToSpeech#LANG_AVAILABLE}, + * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, + * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, + * {@link TextToSpeech#LANG_MISSING_DATA} + * {@link TextToSpeech#LANG_NOT_SUPPORTED}. + */ + int isLanguageAvailable(in String lang, in String country, in String variant); + + /** + * Notifies the engine that it should load a speech synthesis language. + * + * @param lang ISO-3 language code. + * @param country ISO-3 country code. May be empty or null. + * @param variant Language variant. May be empty or null. + * @return Code indicating the support status for the locale. + * One of {@link TextToSpeech#LANG_AVAILABLE}, + * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, + * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, + * {@link TextToSpeech#LANG_MISSING_DATA} + * {@link TextToSpeech#LANG_NOT_SUPPORTED}. + */ + int loadLanguage(in String lang, in String country, in String variant); + + /** + * Sets the callback that will be notified when playback of utterance from the + * given app are completed. + * + * @param callingApp Package name for the app whose utterance the callback will handle. + * @param cb The callback. + */ + void setCallback(in String callingApp, ITextToSpeechCallback cb); + +} diff --git a/core/java/android/speech/tts/ITts.aidl b/core/java/android/speech/tts/ITts.aidl deleted file mode 100755 index c1051c4..0000000 --- a/core/java/android/speech/tts/ITts.aidl +++ /dev/null @@ -1,69 +0,0 @@ -/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.speech.tts;
-
-import android.speech.tts.ITtsCallback;
-
-import android.content.Intent;
-
-/**
- * AIDL for the TTS Service
- * ITts.java is autogenerated from this.
- *
- * {@hide}
- */
-interface ITts {
- int setSpeechRate(in String callingApp, in int speechRate);
-
- int setPitch(in String callingApp, in int pitch);
-
- int speak(in String callingApp, in String text, in int queueMode, in String[] params);
-
- boolean isSpeaking();
-
- int stop(in String callingApp);
-
- void addSpeech(in String callingApp, in String text, in String packageName, in int resId);
-
- void addSpeechFile(in String callingApp, in String text, in String filename);
-
- String[] getLanguage();
-
- int isLanguageAvailable(in String language, in String country, in String variant, in String[] params);
-
- int setLanguage(in String callingApp, in String language, in String country, in String variant);
-
- boolean synthesizeToFile(in String callingApp, in String text, in String[] params, in String outputDirectory);
-
- int playEarcon(in String callingApp, in String earcon, in int queueMode, in String[] params);
-
- void addEarcon(in String callingApp, in String earcon, in String packageName, in int resId);
-
- void addEarconFile(in String callingApp, in String earcon, in String filename);
-
- int registerCallback(in String callingApp, ITtsCallback cb);
-
- int unregisterCallback(in String callingApp, ITtsCallback cb);
-
- int playSilence(in String callingApp, in long duration, in int queueMode, in String[] params); -
- int setEngineByPackageName(in String enginePackageName); - - String getDefaultEngine(); - - boolean areDefaultsEnforced();
-}
diff --git a/core/java/android/speech/tts/PlaybackSynthesisRequest.java b/core/java/android/speech/tts/PlaybackSynthesisRequest.java new file mode 100644 index 0000000..6f4c15b --- /dev/null +++ b/core/java/android/speech/tts/PlaybackSynthesisRequest.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.media.AudioFormat; +import android.media.AudioTrack; +import android.os.Bundle; +import android.util.Log; + +/** + * Speech synthesis request that plays the audio as it is received. + */ +class PlaybackSynthesisRequest extends SynthesisRequest { + + private static final String TAG = "PlaybackSynthesisRequest"; + private static final boolean DBG = false; + + private static final int MIN_AUDIO_BUFFER_SIZE = 8192; + + /** + * Audio stream type. Must be one of the STREAM_ contants defined in + * {@link android.media.AudioManager}. + */ + private final int mStreamType; + + /** + * Volume, in the range [0.0f, 1.0f]. The default value is + * {@link TextToSpeech.Engine#DEFAULT_VOLUME} (1.0f). + */ + private final float mVolume; + + /** + * Left/right position of the audio, in the range [-1.0f, 1.0f]. + * The default value is {@link TextToSpeech.Engine#DEFAULT_PAN} (0.0f). + */ + private final float mPan; + + private final Object mStateLock = new Object(); + private AudioTrack mAudioTrack = null; + private boolean mStopped = false; + private boolean mDone = false; + + PlaybackSynthesisRequest(String text, Bundle params, + int streamType, float volume, float pan) { + super(text, params); + mStreamType = streamType; + mVolume = volume; + mPan = pan; + } + + @Override + void stop() { + if (DBG) Log.d(TAG, "stop()"); + synchronized (mStateLock) { + mStopped = true; + cleanUp(); + } + } + + private void cleanUp() { + if (DBG) Log.d(TAG, "cleanUp()"); + if (mAudioTrack != null) { + mAudioTrack.flush(); + mAudioTrack.stop(); + mAudioTrack.release(); + mAudioTrack = null; + } + } + + @Override + public int getMaxBufferSize() { + // The AudioTrack buffer will be at least MIN_AUDIO_BUFFER_SIZE, so that should always be + // a safe buffer size to pass in. + return MIN_AUDIO_BUFFER_SIZE; + } + + @Override + boolean isDone() { + return mDone; + } + + // TODO: add a thread that writes to the AudioTrack? + @Override + public int start(int sampleRateInHz, int audioFormat, int channelCount) { + if (DBG) { + Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat + + "," + channelCount + ")"); + } + + synchronized (mStateLock) { + if (mStopped) { + if (DBG) Log.d(TAG, "Request has been aborted."); + return TextToSpeech.ERROR; + } + if (mAudioTrack != null) { + Log.e(TAG, "start() called twice"); + cleanUp(); + return TextToSpeech.ERROR; + } + + mAudioTrack = createStreamingAudioTrack(sampleRateInHz, audioFormat, channelCount); + if (mAudioTrack == null) { + return TextToSpeech.ERROR; + } + } + + return TextToSpeech.SUCCESS; + } + + private void setupVolume(AudioTrack audioTrack, float volume, float pan) { + float vol = clip(volume, 0.0f, 1.0f); + float panning = clip(pan, -1.0f, 1.0f); + float volLeft = vol; + float volRight = vol; + if (panning > 0.0f) { + volLeft *= (1.0f - panning); + } else if (panning < 0.0f) { + volRight *= (1.0f + panning); + } + if (DBG) Log.d(TAG, "volLeft=" + volLeft + ",volRight=" + volRight); + if (audioTrack.setStereoVolume(volLeft, volRight) != AudioTrack.SUCCESS) { + Log.e(TAG, "Failed to set volume"); + } + } + + private float clip(float value, float min, float max) { + return value > max ? max : (value < min ? min : value); + } + + @Override + public int audioAvailable(byte[] buffer, int offset, int length) { + if (DBG) { + Log.d(TAG, "audioAvailable(byte[" + buffer.length + "]," + + offset + "," + length + ")"); + } + if (length > getMaxBufferSize()) { + throw new IllegalArgumentException("buffer is too large (" + length + " bytes)"); + } + synchronized (mStateLock) { + if (mStopped) { + if (DBG) Log.d(TAG, "Request has been aborted."); + return TextToSpeech.ERROR; + } + if (mAudioTrack == null) { + Log.e(TAG, "audioAvailable(): Not started"); + return TextToSpeech.ERROR; + } + int playState = mAudioTrack.getPlayState(); + if (playState == AudioTrack.PLAYSTATE_STOPPED) { + if (DBG) Log.d(TAG, "AudioTrack stopped, restarting"); + mAudioTrack.play(); + } + // TODO: loop until all data is written? + if (DBG) Log.d(TAG, "AudioTrack.write()"); + int count = mAudioTrack.write(buffer, offset, length); + if (DBG) Log.d(TAG, "AudioTrack.write() returned " + count); + if (count < 0) { + Log.e(TAG, "Writing to AudioTrack failed: " + count); + cleanUp(); + return TextToSpeech.ERROR; + } else { + return TextToSpeech.SUCCESS; + } + } + } + + @Override + public int done() { + if (DBG) Log.d(TAG, "done()"); + synchronized (mStateLock) { + if (mStopped) { + if (DBG) Log.d(TAG, "Request has been aborted."); + return TextToSpeech.ERROR; + } + if (mAudioTrack == null) { + Log.e(TAG, "done(): Not started"); + return TextToSpeech.ERROR; + } + mDone = true; + cleanUp(); + } + return TextToSpeech.SUCCESS; + } + + @Override + public void error() { + if (DBG) Log.d(TAG, "error()"); + synchronized (mStateLock) { + cleanUp(); + } + } + + @Override + public int completeAudioAvailable(int sampleRateInHz, int audioFormat, int channelCount, + byte[] buffer, int offset, int length) { + if (DBG) { + Log.d(TAG, "completeAudioAvailable(" + sampleRateInHz + "," + audioFormat + + "," + channelCount + "byte[" + buffer.length + "]," + + offset + "," + length + ")"); + } + + synchronized (mStateLock) { + if (mStopped) { + if (DBG) Log.d(TAG, "Request has been aborted."); + return TextToSpeech.ERROR; + } + if (mAudioTrack != null) { + Log.e(TAG, "start() called before completeAudioAvailable()"); + cleanUp(); + return TextToSpeech.ERROR; + } + + int channelConfig = getChannelConfig(channelCount); + if (channelConfig < 0) { + Log.e(TAG, "Unsupported number of channels :" + channelCount); + cleanUp(); + return TextToSpeech.ERROR; + } + int bytesPerFrame = getBytesPerFrame(audioFormat); + if (bytesPerFrame < 0) { + Log.e(TAG, "Unsupported audio format :" + audioFormat); + cleanUp(); + return TextToSpeech.ERROR; + } + + mAudioTrack = new AudioTrack(mStreamType, sampleRateInHz, channelConfig, + audioFormat, buffer.length, AudioTrack.MODE_STATIC); + if (mAudioTrack == null) { + return TextToSpeech.ERROR; + } + + try { + mAudioTrack.write(buffer, offset, length); + setupVolume(mAudioTrack, mVolume, mPan); + mAudioTrack.play(); + blockUntilDone(mAudioTrack, bytesPerFrame, length); + mDone = true; + if (DBG) Log.d(TAG, "Wrote data to audio track succesfully : " + length); + } catch (IllegalStateException ex) { + Log.e(TAG, "Playback error", ex); + return TextToSpeech.ERROR; + } finally { + cleanUp(); + } + } + + return TextToSpeech.SUCCESS; + } + + private void blockUntilDone(AudioTrack audioTrack, int bytesPerFrame, int length) { + int lengthInFrames = length / bytesPerFrame; + int currentPosition = 0; + while ((currentPosition = audioTrack.getPlaybackHeadPosition()) < lengthInFrames) { + long estimatedTimeMs = ((lengthInFrames - currentPosition) * 1000) / + audioTrack.getSampleRate(); + if (DBG) Log.d(TAG, "About to sleep for : " + estimatedTimeMs + " ms," + + " Playback position : " + currentPosition); + try { + Thread.sleep(estimatedTimeMs); + } catch (InterruptedException ie) { + break; + } + } + } + + private int getBytesPerFrame(int audioFormat) { + if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) { + return 1; + } else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) { + return 2; + } + + return -1; + } + + private int getChannelConfig(int channelCount) { + if (channelCount == 1) { + return AudioFormat.CHANNEL_OUT_MONO; + } else if (channelCount == 2){ + return AudioFormat.CHANNEL_OUT_STEREO; + } + + return -1; + } + + private AudioTrack createStreamingAudioTrack(int sampleRateInHz, int audioFormat, + int channelCount) { + int channelConfig = getChannelConfig(channelCount); + + if (channelConfig < 0) { + Log.e(TAG, "Unsupported number of channels : " + channelCount); + return null; + } + + int minBufferSizeInBytes + = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); + int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes); + AudioTrack audioTrack = new AudioTrack(mStreamType, sampleRateInHz, channelConfig, + audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM); + if (audioTrack == null) { + return null; + } + + if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) { + audioTrack.release(); + return null; + } + setupVolume(audioTrack, mVolume, mPan); + return audioTrack; + } +} diff --git a/core/java/android/speech/tts/SynthesisRequest.java b/core/java/android/speech/tts/SynthesisRequest.java new file mode 100644 index 0000000..57ae10d --- /dev/null +++ b/core/java/android/speech/tts/SynthesisRequest.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.os.Bundle; + +/** + * A request for speech synthesis given to a TTS engine for processing. + * + * The engine can provide streaming audio by calling + * {@link #start}, then {@link #audioAvailable} until all audio has been provided, then finally + * {@link #done}. + * + * Alternatively, the engine can provide all the audio at once, by using + * {@link #completeAudioAvailable}. + */ +public abstract class SynthesisRequest { + + private final String mText; + private final Bundle mParams; + private String mLanguage; + private String mCountry; + private String mVariant; + private int mSpeechRate; + private int mPitch; + + public SynthesisRequest(String text, Bundle params) { + mText = text; + mParams = new Bundle(params); + } + + /** + * Sets the locale for the request. + */ + void setLanguage(String language, String country, String variant) { + mLanguage = language; + mCountry = country; + mVariant = variant; + } + + /** + * Sets the speech rate. + */ + void setSpeechRate(int speechRate) { + mSpeechRate = speechRate; + } + + /** + * Sets the pitch. + */ + void setPitch(int pitch) { + mPitch = pitch; + } + + /** + * Gets the text which should be synthesized. + */ + public String getText() { + return mText; + } + + /** + * Gets the ISO 3-letter language code for the language to use. + */ + public String getLanguage() { + return mLanguage; + } + + /** + * Gets the ISO 3-letter country code for the language to use. + */ + public String getCountry() { + return mCountry; + } + + /** + * Gets the language variant to use. + */ + public String getVariant() { + return mVariant; + } + + /** + * Gets the speech rate to use. The normal rate is 100. + */ + public int getSpeechRate() { + return mSpeechRate; + } + + /** + * Gets the pitch to use. The normal pitch is 100. + */ + public int getPitch() { + return mPitch; + } + + /** + * Gets the additional params, if any. + */ + public Bundle getParams() { + return mParams; + } + + /** + * Gets the maximum number of bytes that the TTS engine can pass in a single call of + * {@link #audioAvailable}. This does not apply to {@link #completeAudioAvailable}. + */ + public abstract int getMaxBufferSize(); + + /** + * Checks whether the synthesis request completed successfully. + */ + abstract boolean isDone(); + + /** + * Aborts the speech request. + * + * Can be called from multiple threads. + */ + abstract void stop(); + + /** + * The service should call this when it starts to synthesize audio for this + * request. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText}. + * + * @param sampleRateInHz Sample rate in HZ of the generated audio. + * @param audioFormat Audio format of the generated audio. Must be one of + * the ENCODING_ constants defined in {@link android.media.AudioFormat}. + * @param channelCount The number of channels. Must be {@code 1} or {@code 2}. + * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + */ + public abstract int start(int sampleRateInHz, int audioFormat, int channelCount); + + /** + * The service should call this method when synthesized audio is ready for consumption. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText}. + * + * @param buffer The generated audio data. This method will not hold on to {@code buffer}, + * so the caller is free to modify it after this method returns. + * @param offset The offset into {@code buffer} where the audio data starts. + * @param length The number of bytes of audio data in {@code buffer}. This must be + * less than or equal to the return value of {@link #getMaxBufferSize}. + * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + */ + public abstract int audioAvailable(byte[] buffer, int offset, int length); + + /** + * The service should call this method when all the synthesized audio for a request has + * been passed to {@link #audioAvailable}. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText}. + * + * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + */ + public abstract int done(); + + /** + * The service should call this method if the speech synthesis fails. + * + * This method should only be called on the synthesis thread, + * while in {@link TextToSpeechService#onSynthesizeText}. + */ + public abstract void error(); + + /** + * The service can call this method instead of using {@link #start}, {@link #audioAvailable} + * and {@link #done} if all the audio data is available in a single buffer. + * + * @param sampleRateInHz Sample rate in HZ of the generated audio. + * @param audioFormat Audio format of the generated audio. Must be one of + * the ENCODING_ constants defined in {@link android.media.AudioFormat}. + * @param channelCount The number of channels. Must be {@code 1} or {@code 2}. + * @param buffer The generated audio data. This method will not hold on to {@code buffer}, + * so the caller is free to modify it after this method returns. + * @param offset The offset into {@code buffer} where the audio data starts. + * @param length The number of bytes of audio data in {@code buffer}. + * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + */ + public abstract int completeAudioAvailable(int sampleRateInHz, int audioFormat, + int channelCount, byte[] buffer, int offset, int length); +}
\ No newline at end of file diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index 186af70..4b19469 100755 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009 Google Inc. + * Copyright (C) 2009 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 @@ -15,22 +15,31 @@ */ package android.speech.tts; -import android.speech.tts.ITts; -import android.speech.tts.ITtsCallback; - import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.media.AudioManager; +import android.net.Uri; +import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; /** * @@ -44,14 +53,16 @@ import java.util.Locale; */ public class TextToSpeech { + private static final String TAG = "TextToSpeech"; + /** * Denotes a successful operation. */ - public static final int SUCCESS = 0; + public static final int SUCCESS = 0; /** * Denotes a generic operation failure. */ - public static final int ERROR = -1; + public static final int ERROR = -1; /** * Queue mode where all entries in the playback queue (media to be played @@ -63,22 +74,19 @@ public class TextToSpeech { */ public static final int QUEUE_ADD = 1; - /** * Denotes the language is available exactly as specified by the locale. */ public static final int LANG_COUNTRY_VAR_AVAILABLE = 2; - /** - * Denotes the language is available for the language and country specified + * Denotes the language is available for the language and country specified * by the locale, but not the variant. */ public static final int LANG_COUNTRY_AVAILABLE = 1; - /** - * Denotes the language is available for the language by the locale, + * Denotes the language is available for the language by the locale, * but not the country and variant. */ public static final int LANG_AVAILABLE = 0; @@ -93,7 +101,6 @@ public class TextToSpeech { */ public static final int LANG_NOT_SUPPORTED = -2; - /** * Broadcast Action: The TextToSpeech synthesizer has completed processing * of all the text in the speech queue. @@ -102,7 +109,6 @@ public class TextToSpeech { public static final String ACTION_TTS_QUEUE_PROCESSING_COMPLETED = "android.speech.tts.TTS_QUEUE_PROCESSING_COMPLETED"; - /** * Interface definition of a callback to be invoked indicating the completion of the * TextToSpeech engine initialization. @@ -110,103 +116,115 @@ public class TextToSpeech { public interface OnInitListener { /** * Called to signal the completion of the TextToSpeech engine initialization. + * * @param status {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. */ public void onInit(int status); } /** - * Interface definition of a callback to be invoked indicating the TextToSpeech engine has - * completed synthesizing an utterance with an utterance ID set. - * + * Listener that will be called when the TTS service has + * completed synthesizing an utterance. This is only called if the utterance + * has an utterance ID (see {@link TextToSpeech.Engine#KEY_PARAM_UTTERANCE_ID}). */ public interface OnUtteranceCompletedListener { /** - * Called to signal the completion of the synthesis of the utterance that was identified - * with the string parameter. This identifier is the one originally passed in the - * parameter hashmap of the synthesis request in - * {@link TextToSpeech#speak(String, int, HashMap)} or - * {@link TextToSpeech#synthesizeToFile(String, HashMap, String)} with the - * {@link TextToSpeech.Engine#KEY_PARAM_UTTERANCE_ID} key. + * Called when an utterance has been synthesized. + * * @param utteranceId the identifier of the utterance. */ public void onUtteranceCompleted(String utteranceId); } - /** - * Internal constants for the TextToSpeech functionality - * + * Constants and parameter names for controlling text-to-speech. */ public class Engine { - // default values for a TTS engine when settings are not found in the provider + /** - * {@hide} + * Default speech rate. + * @hide */ - public static final int DEFAULT_RATE = 100; // 1x + public static final int DEFAULT_RATE = 100; + /** - * {@hide} + * Default pitch. + * @hide */ - public static final int DEFAULT_PITCH = 100;// 1x + public static final int DEFAULT_PITCH = 100; + /** - * {@hide} + * Default volume. + * @hide */ public static final float DEFAULT_VOLUME = 1.0f; + /** - * {@hide} - */ - protected static final String DEFAULT_VOLUME_STRING = "1.0"; - /** - * {@hide} + * Default pan (centered). + * @hide */ public static final float DEFAULT_PAN = 0.0f; - /** - * {@hide} - */ - protected static final String DEFAULT_PAN_STRING = "0.0"; /** - * {@hide} + * Default value for {@link Settings.Secure#TTS_USE_DEFAULTS}. + * @hide */ public static final int USE_DEFAULTS = 0; // false + /** - * {@hide} + * Package name of the default TTS engine. + * + * TODO: This should come from a system property + * + * @hide */ - public static final String DEFAULT_SYNTH = "com.svox.pico"; + public static final String DEFAULT_ENGINE = "com.svox.pico"; - // default values for rendering /** * Default audio stream used when playing synthesized speech. */ public static final int DEFAULT_STREAM = AudioManager.STREAM_MUSIC; - // return codes for a TTS engine's check data activity /** * Indicates success when checking the installation status of the resources used by the * TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_PASS = 1; + /** * Indicates failure when checking the installation status of the resources used by the * TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_FAIL = 0; + /** * Indicates erroneous data when checking the installation status of the resources used by * the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_BAD_DATA = -1; + /** * Indicates missing resources when checking the installation status of the resources used * by the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_MISSING_DATA = -2; + /** * Indicates missing storage volume when checking the installation status of the resources * used by the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_MISSING_VOLUME = -3; + /** + * Intent for starting a TTS service. Services that handle this intent must + * extend {@link TextToSpeechService}. Normal applications should not use this intent + * directly, instead they should talk to the TTS service using the the methods in this + * class. + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String INTENT_ACTION_TTS_SERVICE = + "android.intent.action.TTS_SERVICE"; + // intents to ask engine to install data or check its data /** * Activity Action: Triggers the platform TextToSpeech engine to @@ -229,6 +247,7 @@ public class TextToSpeech { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_TTS_DATA_INSTALLED = "android.speech.tts.engine.TTS_DATA_INSTALLED"; + /** * Activity Action: Starts the activity from the platform TextToSpeech * engine to verify the proper installation and availability of the @@ -256,23 +275,36 @@ public class TextToSpeech { public static final String ACTION_CHECK_TTS_DATA = "android.speech.tts.engine.CHECK_TTS_DATA"; + /** + * Activity intent for getting some sample text to use for demonstrating TTS. + * + * @hide This intent was used by engines written against the old API. + * Not sure if it should be exposed. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_GET_SAMPLE_TEXT = + "android.speech.tts.engine.GET_SAMPLE_TEXT"; + // extras for a TTS engine's check data activity /** * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where * the TextToSpeech engine specifies the path to its resources. */ public static final String EXTRA_VOICE_DATA_ROOT_DIRECTORY = "dataRoot"; + /** * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where * the TextToSpeech engine specifies the file names of its resources under the * resource path. */ public static final String EXTRA_VOICE_DATA_FILES = "dataFiles"; + /** * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where * the TextToSpeech engine specifies the locale associated with each resource file. */ public static final String EXTRA_VOICE_DATA_FILES_INFO = "dataFilesInfo"; + /** * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where * the TextToSpeech engine returns an ArrayList<String> of all the available voices. @@ -280,6 +312,7 @@ public class TextToSpeech { * optional (ie, "eng" or "eng-USA" or "eng-USA-FEMALE"). */ public static final String EXTRA_AVAILABLE_VOICES = "availableVoices"; + /** * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where * the TextToSpeech engine returns an ArrayList<String> of all the unavailable voices. @@ -287,6 +320,7 @@ public class TextToSpeech { * optional (ie, "eng" or "eng-USA" or "eng-USA-FEMALE"). */ public static final String EXTRA_UNAVAILABLE_VOICES = "unavailableVoices"; + /** * Extra information sent with the {@link #ACTION_CHECK_TTS_DATA} intent where the * caller indicates to the TextToSpeech engine which specific sets of voice data to @@ -309,137 +343,105 @@ public class TextToSpeech { // keys for the parameters passed with speak commands. Hidden keys are used internally // to maintain engine state for each TextToSpeech instance. /** - * {@hide} + * @hide */ public static final String KEY_PARAM_RATE = "rate"; + /** - * {@hide} + * @hide */ public static final String KEY_PARAM_LANGUAGE = "language"; + /** - * {@hide} + * @hide */ public static final String KEY_PARAM_COUNTRY = "country"; + /** - * {@hide} + * @hide */ public static final String KEY_PARAM_VARIANT = "variant"; + /** - * {@hide} + * @hide */ public static final String KEY_PARAM_ENGINE = "engine"; + /** - * {@hide} + * @hide */ public static final String KEY_PARAM_PITCH = "pitch"; + /** * Parameter key to specify the audio stream type to be used when speaking text - * or playing back a file. + * or playing back a file. The value should be one of the STREAM_ constants + * defined in {@link AudioManager}. + * * @see TextToSpeech#speak(String, int, HashMap) * @see TextToSpeech#playEarcon(String, int, HashMap) */ public static final String KEY_PARAM_STREAM = "streamType"; + /** * Parameter key to identify an utterance in the * {@link TextToSpeech.OnUtteranceCompletedListener} after text has been * spoken, a file has been played back or a silence duration has elapsed. + * * @see TextToSpeech#speak(String, int, HashMap) * @see TextToSpeech#playEarcon(String, int, HashMap) * @see TextToSpeech#synthesizeToFile(String, HashMap, String) */ public static final String KEY_PARAM_UTTERANCE_ID = "utteranceId"; + /** * Parameter key to specify the speech volume relative to the current stream type * volume used when speaking text. Volume is specified as a float ranging from 0 to 1 * where 0 is silence, and 1 is the maximum volume (the default behavior). + * * @see TextToSpeech#speak(String, int, HashMap) * @see TextToSpeech#playEarcon(String, int, HashMap) */ public static final String KEY_PARAM_VOLUME = "volume"; + /** * Parameter key to specify how the speech is panned from left to right when speaking text. * Pan is specified as a float ranging from -1 to +1 where -1 maps to a hard-left pan, * 0 to center (the default behavior), and +1 to hard-right. + * * @see TextToSpeech#speak(String, int, HashMap) * @see TextToSpeech#playEarcon(String, int, HashMap) */ public static final String KEY_PARAM_PAN = "pan"; - // key positions in the array of cached parameters - /** - * {@hide} - */ - protected static final int PARAM_POSITION_RATE = 0; - /** - * {@hide} - */ - protected static final int PARAM_POSITION_LANGUAGE = 2; - /** - * {@hide} - */ - protected static final int PARAM_POSITION_COUNTRY = 4; - /** - * {@hide} - */ - protected static final int PARAM_POSITION_VARIANT = 6; - /** - * {@hide} - */ - protected static final int PARAM_POSITION_STREAM = 8; - /** - * {@hide} - */ - protected static final int PARAM_POSITION_UTTERANCE_ID = 10; - - /** - * {@hide} - */ - protected static final int PARAM_POSITION_ENGINE = 12; - - /** - * {@hide} - */ - protected static final int PARAM_POSITION_PITCH = 14; - - /** - * {@hide} - */ - protected static final int PARAM_POSITION_VOLUME = 16; - - /** - * {@hide} - */ - protected static final int PARAM_POSITION_PAN = 18; - - - /** - * {@hide} - * Total number of cached speech parameters. - * This number should be equal to (max param position/2) + 1. - */ - protected static final int NB_CACHED_PARAMS = 10; } - /** - * Connection needed for the TTS. - */ - private ServiceConnection mServiceConnection; - - private ITts mITts = null; - private ITtsCallback mITtscallback = null; - private Context mContext = null; - private String mPackageName = ""; - private OnInitListener mInitListener = null; - private boolean mStarted = false; + private final Context mContext; + private Connection mServiceConnection; + private OnInitListener mInitListener; private final Object mStartLock = new Object(); + + private String mRequestedEngine; + private final Map<String, Uri> mEarcons; + private final Map<String, Uri> mUtterances; + private final Bundle mParams = new Bundle(); + private String mCurrentEngine = null; + /** - * Used to store the cached parameters sent along with each synthesis request to the - * TTS service. + * The constructor for the TextToSpeech class, using the default TTS engine. + * This will also initialize the associated TextToSpeech engine if it isn't already running. + * + * @param context + * The context this instance is running in. + * @param listener + * The {@link TextToSpeech.OnInitListener} that will be called when the + * TextToSpeech engine has initialized. */ - private String[] mCachedParams; + public TextToSpeech(Context context, OnInitListener listener) { + this(context, listener, null); + } /** - * The constructor for the TextToSpeech class. + * The constructor for the TextToSpeech class, using the given TTS engine. * This will also initialize the associated TextToSpeech engine if it isn't already running. * * @param context @@ -447,86 +449,95 @@ public class TextToSpeech { * @param listener * The {@link TextToSpeech.OnInitListener} that will be called when the * TextToSpeech engine has initialized. + * @param engine Package name of the TTS engine to use. */ - public TextToSpeech(Context context, OnInitListener listener) { + public TextToSpeech(Context context, OnInitListener listener, String engine) { mContext = context; - mPackageName = mContext.getPackageName(); mInitListener = listener; + mRequestedEngine = engine; - mCachedParams = new String[2*Engine.NB_CACHED_PARAMS]; // store key and value - mCachedParams[Engine.PARAM_POSITION_RATE] = Engine.KEY_PARAM_RATE; - mCachedParams[Engine.PARAM_POSITION_LANGUAGE] = Engine.KEY_PARAM_LANGUAGE; - mCachedParams[Engine.PARAM_POSITION_COUNTRY] = Engine.KEY_PARAM_COUNTRY; - mCachedParams[Engine.PARAM_POSITION_VARIANT] = Engine.KEY_PARAM_VARIANT; - mCachedParams[Engine.PARAM_POSITION_STREAM] = Engine.KEY_PARAM_STREAM; - mCachedParams[Engine.PARAM_POSITION_UTTERANCE_ID] = Engine.KEY_PARAM_UTTERANCE_ID; - mCachedParams[Engine.PARAM_POSITION_ENGINE] = Engine.KEY_PARAM_ENGINE; - mCachedParams[Engine.PARAM_POSITION_PITCH] = Engine.KEY_PARAM_PITCH; - mCachedParams[Engine.PARAM_POSITION_VOLUME] = Engine.KEY_PARAM_VOLUME; - mCachedParams[Engine.PARAM_POSITION_PAN] = Engine.KEY_PARAM_PAN; - - // Leave all defaults that are shown in Settings uninitialized/at the default - // so that the values set in Settings will take effect if the application does - // not try to change these settings itself. - mCachedParams[Engine.PARAM_POSITION_RATE + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_LANGUAGE + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_COUNTRY + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_VARIANT + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_STREAM + 1] = - String.valueOf(Engine.DEFAULT_STREAM); - mCachedParams[Engine.PARAM_POSITION_UTTERANCE_ID + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_ENGINE + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_PITCH + 1] = "100"; - mCachedParams[Engine.PARAM_POSITION_VOLUME + 1] = Engine.DEFAULT_VOLUME_STRING; - mCachedParams[Engine.PARAM_POSITION_PAN + 1] = Engine.DEFAULT_PAN_STRING; + mEarcons = new HashMap<String, Uri>(); + mUtterances = new HashMap<String, Uri>(); initTts(); } + private String getPackageName() { + return mContext.getPackageName(); + } - private void initTts() { - mStarted = false; - - // Initialize the TTS, run the callback after the binding is successful - mServiceConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName name, IBinder service) { - synchronized(mStartLock) { - mITts = ITts.Stub.asInterface(service); - mStarted = true; - // Cache the default engine and current language - setEngineByPackageName(getDefaultEngine()); - setLanguage(getLanguage()); - if (mInitListener != null) { - // TODO manage failures and missing resources - mInitListener.onInit(SUCCESS); - } - } + private <R> R runActionNoReconnect(Action<R> action, R errorResult, String method) { + return runAction(action, errorResult, method, false); + } + + private <R> R runAction(Action<R> action, R errorResult, String method) { + return runAction(action, errorResult, method, true); + } + + private <R> R runAction(Action<R> action, R errorResult, String method, boolean reconnect) { + synchronized (mStartLock) { + if (mServiceConnection == null) { + Log.w(TAG, method + " failed: not bound to TTS engine"); + return errorResult; } + return mServiceConnection.runAction(action, errorResult, method, reconnect); + } + } - public void onServiceDisconnected(ComponentName name) { - synchronized(mStartLock) { - mITts = null; - mInitListener = null; - mStarted = false; - } + private int initTts() { + String defaultEngine = getDefaultEngine(); + String engine = defaultEngine; + if (!areDefaultsEnforced() && !TextUtils.isEmpty(mRequestedEngine) + && isEngineEnabled(engine)) { + engine = mRequestedEngine; + } + + // Try requested engine + if (connectToEngine(engine)) { + return SUCCESS; + } + + // Fall back to user's default engine if different from the already tested one + if (!engine.equals(defaultEngine)) { + if (connectToEngine(defaultEngine)) { + return SUCCESS; } - }; + } - Intent intent = new Intent("android.intent.action.START_TTS_SERVICE"); - intent.addCategory("android.intent.category.TTS"); - boolean bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); - if (!bound) { - Log.e("TextToSpeech.java", "initTts() failed to bind to service"); - if (mInitListener != null) { - mInitListener.onInit(ERROR); + // Fall back to the hardcoded default if different from the two above + if (!defaultEngine.equals(Engine.DEFAULT_ENGINE) + && !engine.equals(Engine.DEFAULT_ENGINE)) { + if (connectToEngine(Engine.DEFAULT_ENGINE)) { + return SUCCESS; } + } + + return ERROR; + } + + private boolean connectToEngine(String engine) { + Connection connection = new Connection(); + Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE); + intent.setPackage(engine); + boolean bound = mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE); + if (!bound) { + Log.e(TAG, "Failed to bind to " + engine); + dispatchOnInit(ERROR); + return false; } else { - // initialization listener will be called inside ServiceConnection - Log.i("TextToSpeech.java", "initTts() successfully bound to service"); + mCurrentEngine = engine; + return true; } - // TODO handle plugin failures } + private void dispatchOnInit(int result) { + synchronized (mStartLock) { + if (mInitListener != null) { + mInitListener.onInit(result); + mInitListener = null; + } + } + } /** * Releases the resources used by the TextToSpeech engine. @@ -534,15 +545,17 @@ public class TextToSpeech { * so the TextToSpeech engine can be cleanly stopped. */ public void shutdown() { - try { - mContext.unbindService(mServiceConnection); - } catch (IllegalArgumentException e) { - // Do nothing and fail silently since an error here indicates that - // binding never succeeded in the first place. - } + runActionNoReconnect(new Action<Void>() { + @Override + public Void run(ITextToSpeechService service) throws RemoteException { + service.setCallback(getPackageName(), null); + service.stop(getPackageName()); + mServiceConnection.disconnect(); + return null; + } + }, null, "shutdown"); } - /** * Adds a mapping between a string of text and a sound resource in a * package. After a call to this method, subsequent calls to @@ -571,37 +584,12 @@ public class TextToSpeech { * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ public int addSpeech(String text, String packagename, int resourceId) { - synchronized(mStartLock) { - if (!mStarted) { - return ERROR; - } - try { - mITts.addSpeech(mPackageName, text, packagename, resourceId); - return SUCCESS; - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addSpeech", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addSpeech", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addSpeech", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } - return ERROR; + synchronized (mStartLock) { + mUtterances.put(text, makeResourceUri(packagename, resourceId)); + return SUCCESS; } } - /** * Adds a mapping between a string of text and a sound file. Using this, it * is possible to add custom pronounciations for a string of text. @@ -619,32 +607,8 @@ public class TextToSpeech { */ public int addSpeech(String text, String filename) { synchronized (mStartLock) { - if (!mStarted) { - return ERROR; - } - try { - mITts.addSpeechFile(mPackageName, text, filename); - return SUCCESS; - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addSpeech", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addSpeech", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addSpeech", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } - return ERROR; + mUtterances.put(text, Uri.parse(filename)); + return SUCCESS; } } @@ -676,36 +640,11 @@ public class TextToSpeech { */ public int addEarcon(String earcon, String packagename, int resourceId) { synchronized(mStartLock) { - if (!mStarted) { - return ERROR; - } - try { - mITts.addEarcon(mPackageName, earcon, packagename, resourceId); - return SUCCESS; - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addEarcon", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addEarcon", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addEarcon", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } - return ERROR; + mEarcons.put(earcon, makeResourceUri(packagename, resourceId)); + return SUCCESS; } } - /** * Adds a mapping between a string of text and a sound file. * Use this to add custom earcons. @@ -722,403 +661,211 @@ public class TextToSpeech { * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ public int addEarcon(String earcon, String filename) { - synchronized (mStartLock) { - if (!mStarted) { - return ERROR; - } - try { - mITts.addEarconFile(mPackageName, earcon, filename); - return SUCCESS; - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addEarcon", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addEarcon", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addEarcon", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } - return ERROR; + synchronized(mStartLock) { + mEarcons.put(earcon, Uri.parse(filename)); + return SUCCESS; } } + private Uri makeResourceUri(String packageName, int resourceId) { + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .encodedAuthority(packageName) + .appendEncodedPath(String.valueOf(resourceId)) + .build(); + } /** * Speaks the string using the specified queuing strategy and speech * parameters. * - * @param text - * The string of text to be spoken. - * @param queueMode - * The queuing strategy to use. - * {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. - * @param params - * The list of parameters to be used. Can be null if no parameters are given. - * They are specified using a (key, value) pair, where the key can be - * {@link Engine#KEY_PARAM_STREAM} or - * {@link Engine#KEY_PARAM_UTTERANCE_ID}. + * @param text The string of text to be spoken. + * @param queueMode The queuing strategy to use, {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. + * @param params Parameters for the request. Can be null. + * Supported parameter names: + * {@link Engine#KEY_PARAM_STREAM}, + * {@link Engine#KEY_PARAM_UTTERANCE_ID}, + * {@link Engine#KEY_PARAM_VOLUME}, + * {@link Engine#KEY_PARAM_PAN}. + * Engine specific parameters may be passed in but the parameter keys + * must be prefixed by the name of the engine they are intended for. For example + * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the + * engine named "com.svox.pico" if it is being used. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ - public int speak(String text, int queueMode, HashMap<String,String> params) - { - synchronized (mStartLock) { - int result = ERROR; - Log.i("TextToSpeech.java - speak", "speak text of length " + text.length()); - if (!mStarted) { - Log.e("TextToSpeech.java - speak", "service isn't started"); - return result; - } - try { - if ((params != null) && (!params.isEmpty())) { - setCachedParam(params, Engine.KEY_PARAM_STREAM, Engine.PARAM_POSITION_STREAM); - setCachedParam(params, Engine.KEY_PARAM_UTTERANCE_ID, - Engine.PARAM_POSITION_UTTERANCE_ID); - setCachedParam(params, Engine.KEY_PARAM_ENGINE, Engine.PARAM_POSITION_ENGINE); - setCachedParam(params, Engine.KEY_PARAM_VOLUME, Engine.PARAM_POSITION_VOLUME); - setCachedParam(params, Engine.KEY_PARAM_PAN, Engine.PARAM_POSITION_PAN); + public int speak(final String text, final int queueMode, final HashMap<String, String> params) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + Uri utteranceUri = mUtterances.get(text); + if (utteranceUri != null) { + return service.playAudio(getPackageName(), utteranceUri, queueMode, + getParams(params)); + } else { + return service.speak(getPackageName(), text, queueMode, getParams(params)); } - result = mITts.speak(mPackageName, text, queueMode, mCachedParams); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - speak", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - speak", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - speak", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - resetCachedParams(); - return result; } - } + }, ERROR, "speak"); } - /** * Plays the earcon using the specified queueing mode and parameters. + * The earcon must already have been added with {@link #addEarcon(String, String)} or + * {@link #addEarcon(String, String, int)}. * - * @param earcon - * The earcon that should be played - * @param queueMode - * {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. - * @param params - * The list of parameters to be used. Can be null if no parameters are given. - * They are specified using a (key, value) pair, where the key can be - * {@link Engine#KEY_PARAM_STREAM} or + * @param earcon The earcon that should be played + * @param queueMode {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. + * @param params Parameters for the request. Can be null. + * Supported parameter names: + * {@link Engine#KEY_PARAM_STREAM}, * {@link Engine#KEY_PARAM_UTTERANCE_ID}. + * Engine specific parameters may be passed in but the parameter keys + * must be prefixed by the name of the engine they are intended for. For example + * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the + * engine named "com.svox.pico" if it is being used. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ - public int playEarcon(String earcon, int queueMode, - HashMap<String,String> params) { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; - } - try { - if ((params != null) && (!params.isEmpty())) { - String extra = params.get(Engine.KEY_PARAM_STREAM); - if (extra != null) { - mCachedParams[Engine.PARAM_POSITION_STREAM + 1] = extra; - } - setCachedParam(params, Engine.KEY_PARAM_STREAM, Engine.PARAM_POSITION_STREAM); - setCachedParam(params, Engine.KEY_PARAM_UTTERANCE_ID, - Engine.PARAM_POSITION_UTTERANCE_ID); + public int playEarcon(final String earcon, final int queueMode, + final HashMap<String, String> params) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + Uri earconUri = mEarcons.get(earcon); + if (earconUri == null) { + return ERROR; } - result = mITts.playEarcon(mPackageName, earcon, queueMode, null); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - playEarcon", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - playEarcon", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - playEarcon", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - resetCachedParams(); - return result; + return service.playAudio(getPackageName(), earconUri, queueMode, + getParams(params)); } - } + }, ERROR, "playEarcon"); } /** * Plays silence for the specified amount of time using the specified * queue mode. * - * @param durationInMs - * A long that indicates how long the silence should last. - * @param queueMode - * {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. - * @param params - * The list of parameters to be used. Can be null if no parameters are given. - * They are specified using a (key, value) pair, where the key can be + * @param durationInMs The duration of the silence. + * @param queueMode {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. + * @param params Parameters for the request. Can be null. + * Supported parameter names: * {@link Engine#KEY_PARAM_UTTERANCE_ID}. + * Engine specific parameters may be passed in but the parameter keys + * must be prefixed by the name of the engine they are intended for. For example + * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the + * engine named "com.svox.pico" if it is being used. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ - public int playSilence(long durationInMs, int queueMode, HashMap<String,String> params) { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; - } - try { - if ((params != null) && (!params.isEmpty())) { - setCachedParam(params, Engine.KEY_PARAM_UTTERANCE_ID, - Engine.PARAM_POSITION_UTTERANCE_ID); - } - result = mITts.playSilence(mPackageName, durationInMs, queueMode, mCachedParams); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - playSilence", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - playSilence", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - playSilence", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - resetCachedParams(); - return result; + public int playSilence(final long durationInMs, final int queueMode, + final HashMap<String, String> params) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + return service.playSilence(getPackageName(), durationInMs, queueMode, + getParams(params)); } - } + }, ERROR, "playSilence"); } - /** - * Returns whether or not the TextToSpeech engine is busy speaking. + * Checks whether the TTS engine is busy speaking. * - * @return Whether or not the TextToSpeech engine is busy speaking. + * @return {@code true} if the TTS engine is speaking. */ public boolean isSpeaking() { - synchronized (mStartLock) { - if (!mStarted) { - return false; + return runAction(new Action<Boolean>() { + @Override + public Boolean run(ITextToSpeechService service) throws RemoteException { + return service.isSpeaking(); } - try { - return mITts.isSpeaking(); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - isSpeaking", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - isSpeaking", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - isSpeaking", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } - return false; - } + }, false, "isSpeaking"); } - /** * Interrupts the current utterance (whether played or rendered to file) and discards other * utterances in the queue. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ public int stop() { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; - } - try { - result = mITts.stop(mPackageName); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - stop", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - stop", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - stop", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + return service.stop(getPackageName()); } - } + }, ERROR, "stop"); } - /** - * Sets the speech rate for the TextToSpeech engine. + * Sets the speech rate. * * This has no effect on any pre-recorded speech. * - * @param speechRate - * The speech rate for the TextToSpeech engine. 1 is the normal speed, - * lower values slow down the speech (0.5 is half the normal speech rate), - * greater values accelerate it (2 is twice the normal speech rate). + * @param speechRate Speech rate. {@code 1.0} is the normal speech rate, + * lower values slow down the speech ({@code 0.5} is half the normal speech rate), + * greater values accelerate it ({@code 2.0} is twice the normal speech rate). * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ public int setSpeechRate(float speechRate) { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; - } - try { - if (speechRate > 0) { - int rate = (int)(speechRate*100); - mCachedParams[Engine.PARAM_POSITION_RATE + 1] = String.valueOf(rate); - // the rate is not set here, instead it is cached so it will be associated - // with all upcoming utterances. - if (speechRate > 0.0f) { - result = SUCCESS; - } else { - result = ERROR; - } + if (speechRate > 0.0f) { + int intRate = (int)(speechRate * 100); + if (intRate > 0) { + synchronized (mStartLock) { + mParams.putInt(Engine.KEY_PARAM_RATE, intRate); } - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setSpeechRate", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setSpeechRate", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; + return SUCCESS; } } + return ERROR; } - /** * Sets the speech pitch for the TextToSpeech engine. * * This has no effect on any pre-recorded speech. * - * @param pitch - * The pitch for the TextToSpeech engine. 1 is the normal pitch, + * @param pitch Speech pitch. {@code 1.0} is the normal pitch, * lower values lower the tone of the synthesized voice, * greater values increase it. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ public int setPitch(float pitch) { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; - } - try { - // the pitch is not set here, instead it is cached so it will be associated - // with all upcoming utterances. - if (pitch > 0) { - int p = (int)(pitch*100); - mCachedParams[Engine.PARAM_POSITION_PITCH + 1] = String.valueOf(p); - result = SUCCESS; + if (pitch > 0.0f) { + int intPitch = (int)(pitch * 100); + if (intPitch > 0) { + synchronized (mStartLock) { + mParams.putInt(Engine.KEY_PARAM_PITCH, intPitch); } - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setPitch", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setPitch", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; + return SUCCESS; } } + return ERROR; } - /** - * Sets the language for the TextToSpeech engine. - * The TextToSpeech engine will try to use the closest match to the specified + * Sets the text-to-speech language. + * The TTS engine will try to use the closest match to the specified * language as represented by the Locale, but there is no guarantee that the exact same Locale * will be used. Use {@link #isLanguageAvailable(Locale)} to check the level of support * before choosing the language to use for the next utterances. * - * @param loc - * The locale describing the language to be used. + * @param loc The locale describing the language to be used. * - * @return code indicating the support status for the locale. See {@link #LANG_AVAILABLE}, + * @return Code indicating the support status for the locale. See {@link #LANG_AVAILABLE}, * {@link #LANG_COUNTRY_AVAILABLE}, {@link #LANG_COUNTRY_VAR_AVAILABLE}, * {@link #LANG_MISSING_DATA} and {@link #LANG_NOT_SUPPORTED}. */ - public int setLanguage(Locale loc) { - synchronized (mStartLock) { - int result = LANG_NOT_SUPPORTED; - if (!mStarted) { - return result; - } - if (loc == null) { - return result; - } - try { + public int setLanguage(final Locale loc) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + if (loc == null) { + return LANG_NOT_SUPPORTED; + } String language = loc.getISO3Language(); String country = loc.getISO3Country(); String variant = loc.getVariant(); @@ -1126,386 +873,331 @@ public class TextToSpeech { // the available parts. // Note that the language is not actually set here, instead it is cached so it // will be associated with all upcoming utterances. - result = mITts.isLanguageAvailable(language, country, variant, mCachedParams); + int result = service.loadLanguage(language, country, variant); if (result >= LANG_AVAILABLE){ - mCachedParams[Engine.PARAM_POSITION_LANGUAGE + 1] = language; - if (result >= LANG_COUNTRY_AVAILABLE){ - mCachedParams[Engine.PARAM_POSITION_COUNTRY + 1] = country; - } else { - mCachedParams[Engine.PARAM_POSITION_COUNTRY + 1] = ""; - } - if (result >= LANG_COUNTRY_VAR_AVAILABLE){ - mCachedParams[Engine.PARAM_POSITION_VARIANT + 1] = variant; - } else { - mCachedParams[Engine.PARAM_POSITION_VARIANT + 1] = ""; + if (result < LANG_COUNTRY_VAR_AVAILABLE) { + variant = ""; + if (result < LANG_COUNTRY_AVAILABLE) { + country = ""; + } } + mParams.putString(Engine.KEY_PARAM_LANGUAGE, language); + mParams.putString(Engine.KEY_PARAM_COUNTRY, country); + mParams.putString(Engine.KEY_PARAM_VARIANT, variant); } - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setLanguage", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setLanguage", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setLanguage", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { return result; } - } + }, LANG_NOT_SUPPORTED, "setLanguage"); } - /** * Returns a Locale instance describing the language currently being used by the TextToSpeech * engine. + * * @return language, country (if any) and variant (if any) used by the engine stored in a Locale - * instance, or null is the TextToSpeech engine has failed. + * instance, or {@code null} on error. */ public Locale getLanguage() { - synchronized (mStartLock) { - if (!mStarted) { - return null; - } - try { - // Only do a call to the native synth if there is nothing in the cached params - if (mCachedParams[Engine.PARAM_POSITION_LANGUAGE + 1].length() < 1){ - String[] locStrings = mITts.getLanguage(); - if ((locStrings != null) && (locStrings.length == 3)) { - return new Locale(locStrings[0], locStrings[1], locStrings[2]); - } else { - return null; - } - } else { - return new Locale(mCachedParams[Engine.PARAM_POSITION_LANGUAGE + 1], - mCachedParams[Engine.PARAM_POSITION_COUNTRY + 1], - mCachedParams[Engine.PARAM_POSITION_VARIANT + 1]); + return runAction(new Action<Locale>() { + @Override + public Locale run(ITextToSpeechService service) throws RemoteException { + String[] locStrings = service.getLanguage(); + if (locStrings != null && locStrings.length == 3) { + return new Locale(locStrings[0], locStrings[1], locStrings[2]); } - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - getLanguage", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - getLanguage", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - getLanguage", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); + return null; } - return null; - } + }, null, "getLanguage"); } /** * Checks if the specified language as represented by the Locale is available and supported. * - * @param loc - * The Locale describing the language to be used. + * @param loc The Locale describing the language to be used. * - * @return code indicating the support status for the locale. See {@link #LANG_AVAILABLE}, + * @return Code indicating the support status for the locale. See {@link #LANG_AVAILABLE}, * {@link #LANG_COUNTRY_AVAILABLE}, {@link #LANG_COUNTRY_VAR_AVAILABLE}, * {@link #LANG_MISSING_DATA} and {@link #LANG_NOT_SUPPORTED}. */ - public int isLanguageAvailable(Locale loc) { - synchronized (mStartLock) { - int result = LANG_NOT_SUPPORTED; - if (!mStarted) { - return result; + public int isLanguageAvailable(final Locale loc) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + return service.isLanguageAvailable(loc.getISO3Language(), + loc.getISO3Country(), loc.getVariant()); } - try { - result = mITts.isLanguageAvailable(loc.getISO3Language(), - loc.getISO3Country(), loc.getVariant(), mCachedParams); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - isLanguageAvailable", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - isLanguageAvailable", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - isLanguageAvailable", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; - } - } + }, LANG_NOT_SUPPORTED, "isLanguageAvailable"); } - /** * Synthesizes the given text to a file using the specified parameters. * - * @param text - * The String of text that should be synthesized - * @param params - * The list of parameters to be used. Can be null if no parameters are given. - * They are specified using a (key, value) pair, where the key can be + * @param text Thetext that should be synthesized + * @param params Parameters for the request. Can be null. + * Supported parameter names: * {@link Engine#KEY_PARAM_UTTERANCE_ID}. - * @param filename - * The string that gives the full output filename; it should be + * Engine specific parameters may be passed in but the parameter keys + * must be prefixed by the name of the engine they are intended for. For example + * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the + * engine named "com.svox.pico" if it is being used. + * @param filename Absolute file filename to write the generated audio data to.It should be * something like "/sdcard/myappsounds/mysound.wav". * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ - public int synthesizeToFile(String text, HashMap<String,String> params, - String filename) { - Log.i("TextToSpeech.java", "synthesizeToFile()"); - synchronized (mStartLock) { - int result = ERROR; - Log.i("TextToSpeech.java - synthesizeToFile", "synthesizeToFile text of length " - + text.length()); - if (!mStarted) { - Log.e("TextToSpeech.java - synthesizeToFile", "service isn't started"); - return result; + public int synthesizeToFile(final String text, final HashMap<String, String> params, + final String filename) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + return service.synthesizeToFile(getPackageName(), text, filename, + getParams(params)); } - try { - if ((params != null) && (!params.isEmpty())) { - // no need to read the stream type here - setCachedParam(params, Engine.KEY_PARAM_UTTERANCE_ID, - Engine.PARAM_POSITION_UTTERANCE_ID); - setCachedParam(params, Engine.KEY_PARAM_ENGINE, Engine.PARAM_POSITION_ENGINE); + }, ERROR, "synthesizeToFile"); + } + + private Bundle getParams(HashMap<String, String> params) { + if (params != null && !params.isEmpty()) { + Bundle bundle = new Bundle(mParams); + copyIntParam(bundle, params, Engine.KEY_PARAM_STREAM); + copyStringParam(bundle, params, Engine.KEY_PARAM_UTTERANCE_ID); + copyFloatParam(bundle, params, Engine.KEY_PARAM_VOLUME); + copyFloatParam(bundle, params, Engine.KEY_PARAM_PAN); + + // Copy over all parameters that start with the name of the + // engine that we are currently connected to. The engine is + // free to interpret them as it chooses. + if (!TextUtils.isEmpty(mCurrentEngine)) { + for (Map.Entry<String, String> entry : params.entrySet()) { + final String key = entry.getKey(); + if (key != null && key.startsWith(mCurrentEngine)) { + bundle.putString(key, entry.getValue()); + } } - result = mITts.synthesizeToFile(mPackageName, text, mCachedParams, filename) ? - SUCCESS : ERROR; - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - synthesizeToFile", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - synthesizeToFile", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - synthesizeToFile", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - resetCachedParams(); - return result; } + + return bundle; + } else { + return mParams; } } + private void copyStringParam(Bundle bundle, HashMap<String, String> params, String key) { + String value = params.get(key); + if (value != null) { + bundle.putString(key, value); + } + } - /** - * Convenience method to reset the cached parameters to the current default values - * if they are not persistent between calls to the service. - */ - private void resetCachedParams() { - mCachedParams[Engine.PARAM_POSITION_STREAM + 1] = - String.valueOf(Engine.DEFAULT_STREAM); - mCachedParams[Engine.PARAM_POSITION_UTTERANCE_ID+ 1] = ""; - mCachedParams[Engine.PARAM_POSITION_VOLUME + 1] = Engine.DEFAULT_VOLUME_STRING; - mCachedParams[Engine.PARAM_POSITION_PAN + 1] = Engine.DEFAULT_PAN_STRING; + private void copyIntParam(Bundle bundle, HashMap<String, String> params, String key) { + String valueString = params.get(key); + if (!TextUtils.isEmpty(valueString)) { + try { + int value = Integer.parseInt(valueString); + bundle.putInt(key, value); + } catch (NumberFormatException ex) { + // don't set the value in the bundle + } + } } - /** - * Convenience method to save a parameter in the cached parameter array, at the given index, - * for a property saved in the given hashmap. - */ - private void setCachedParam(HashMap<String,String> params, String key, int keyIndex) { - String extra = params.get(key); - if (extra != null) { - mCachedParams[keyIndex+1] = extra; + private void copyFloatParam(Bundle bundle, HashMap<String, String> params, String key) { + String valueString = params.get(key); + if (!TextUtils.isEmpty(valueString)) { + try { + float value = Float.parseFloat(valueString); + bundle.putFloat(key, value); + } catch (NumberFormatException ex) { + // don't set the value in the bundle + } } } /** - * Sets the OnUtteranceCompletedListener that will fire when an utterance completes. + * Sets the listener that will be notified when synthesis of an utterance completes. * - * @param listener - * The OnUtteranceCompletedListener + * @param listener The listener to use. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ - public int setOnUtteranceCompletedListener( - final OnUtteranceCompletedListener listener) { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; - } - mITtscallback = new ITtsCallback.Stub() { - public void utteranceCompleted(String utteranceId) throws RemoteException { - if (listener != null) { - listener.onUtteranceCompleted(utteranceId); + public int setOnUtteranceCompletedListener(final OnUtteranceCompletedListener listener) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + ITextToSpeechCallback.Stub callback = new ITextToSpeechCallback.Stub() { + public void utteranceCompleted(String utteranceId) { + if (listener != null) { + listener.onUtteranceCompleted(utteranceId); + } } - } - }; - try { - result = mITts.registerCallback(mPackageName, mITtscallback); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - registerCallback", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - registerCallback", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - registerCallback", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; + }; + service.setCallback(getPackageName(), callback); + return SUCCESS; } - } + }, ERROR, "setOnUtteranceCompletedListener"); } /** - * Sets the speech synthesis engine to be used by its packagename. + * Sets the TTS engine to use. * - * @param enginePackageName - * The packagename for the synthesis engine (ie, "com.svox.pico") + * @param enginePackageName The package name for the synthesis engine (e.g. "com.svox.pico") * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ + // TODO: add @Deprecated{This method does not tell the caller when the new engine + // has been initialized. You should create a new TextToSpeech object with the new + // engine instead.} public int setEngineByPackageName(String enginePackageName) { - synchronized (mStartLock) { - int result = TextToSpeech.ERROR; - if (!mStarted) { - return result; - } - try { - result = mITts.setEngineByPackageName(enginePackageName); - if (result == TextToSpeech.SUCCESS){ - mCachedParams[Engine.PARAM_POSITION_ENGINE + 1] = enginePackageName; - } - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; + mRequestedEngine = enginePackageName; + return initTts(); + } + + /** + * Gets the package name of the default speech synthesis engine. + * + * @return Package name of the TTS engine that the user has chosen as their default. + */ + public String getDefaultEngine() { + String engine = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.TTS_DEFAULT_SYNTH); + return engine != null ? engine : Engine.DEFAULT_ENGINE; + } + + /** + * Checks whether the user's settings should override settings requested by the calling + * application. + */ + public boolean areDefaultsEnforced() { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.TTS_USE_DEFAULTS, Engine.USE_DEFAULTS) == 1; + } + + private boolean isEngineEnabled(String engine) { + if (Engine.DEFAULT_ENGINE.equals(engine)) { + return true; + } + for (String enabled : getEnabledEngines()) { + if (engine.equals(enabled)) { + return true; } } + return false; } + private String[] getEnabledEngines() { + String str = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.TTS_ENABLED_PLUGINS); + if (TextUtils.isEmpty(str)) { + return new String[0]; + } + return str.split(" "); + } /** - * Gets the packagename of the default speech synthesis engine. + * Gets a list of all installed TTS engines. * - * @return Packagename of the TTS engine that the user has chosen as their default. + * @return A list of engine info objects. The list can be empty, but will never by {@code null}. */ - public String getDefaultEngine() { - synchronized (mStartLock) { - String engineName = ""; - if (!mStarted) { - return engineName; + public List<EngineInfo> getEngines() { + PackageManager pm = mContext.getPackageManager(); + Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE); + List<ResolveInfo> resolveInfos = + pm.queryIntentServices(intent, PackageManager.MATCH_DEFAULT_ONLY); + if (resolveInfos == null) return Collections.emptyList(); + List<EngineInfo> engines = new ArrayList<EngineInfo>(resolveInfos.size()); + for (ResolveInfo resolveInfo : resolveInfos) { + ServiceInfo service = resolveInfo.serviceInfo; + if (service != null) { + EngineInfo engine = new EngineInfo(); + // Using just the package name isn't great, since it disallows having + // multiple engines in the same package, but that's what the existing API does. + engine.name = service.packageName; + CharSequence label = service.loadLabel(pm); + engine.label = TextUtils.isEmpty(label) ? engine.name : label.toString(); + engine.icon = service.getIconResource(); + engines.add(engine); + } + } + return engines; + } + + private class Connection implements ServiceConnection { + private ITextToSpeechService mService; + + public void onServiceConnected(ComponentName name, IBinder service) { + Log.i(TAG, "Connected to " + name); + synchronized(mStartLock) { + if (mServiceConnection != null) { + // Disconnect any previous service connection + mServiceConnection.disconnect(); + } + mServiceConnection = this; + mService = ITextToSpeechService.Stub.asInterface(service); + dispatchOnInit(SUCCESS); } + } + + public void onServiceDisconnected(ComponentName name) { + synchronized(mStartLock) { + mService = null; + // If this is the active connection, clear it + if (mServiceConnection == this) { + mServiceConnection = null; + } + } + } + + public void disconnect() { + mContext.unbindService(this); + } + + public <R> R runAction(Action<R> action, R errorResult, String method, boolean reconnect) { try { - engineName = mITts.getDefaultEngine(); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return engineName; + synchronized (mStartLock) { + if (mService == null) { + Log.w(TAG, method + " failed: not connected to TTS engine"); + return errorResult; + } + return action.run(mService); + } + } catch (RemoteException ex) { + Log.e(TAG, method + " failed", ex); + if (reconnect) { + disconnect(); + initTts(); + } + return errorResult; } } } + private interface Action<R> { + R run(ITextToSpeechService service) throws RemoteException; + } /** - * Returns whether or not the user is forcing their defaults to override the - * Text-To-Speech settings set by applications. + * Information about an installed text-to-speech engine. * - * @return Whether or not defaults are enforced. + * @see TextToSpeech#getEngines */ - public boolean areDefaultsEnforced() { - synchronized (mStartLock) { - boolean defaultsEnforced = false; - if (!mStarted) { - return defaultsEnforced; - } - try { - defaultsEnforced = mITts.areDefaultsEnforced(); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - areDefaultsEnforced", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - areDefaultsEnforced", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - areDefaultsEnforced", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return defaultsEnforced; - } + public static class EngineInfo { + /** + * Engine package name.. + */ + public String name; + /** + * Localized label for the engine. + */ + public String label; + /** + * Icon for the engine. + */ + public int icon; + + @Override + public String toString() { + return "EngineInfo{name=" + name + "}"; } + } } diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java new file mode 100644 index 0000000..f32474f --- /dev/null +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -0,0 +1,713 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.app.Service; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.MessageQueue; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.provider.Settings; +import android.speech.tts.TextToSpeech.Engine; +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; + + +/** + * Abstract base class for TTS engine implementations. + */ +public abstract class TextToSpeechService extends Service { + + private static final boolean DBG = false; + private static final String TAG = "TextToSpeechService"; + + private static final int MAX_SPEECH_ITEM_CHAR_LENGTH = 4000; + private static final String SYNTH_THREAD_NAME = "SynthThread"; + + private SynthHandler mSynthHandler; + + private CallbackMap mCallbacks; + + @Override + public void onCreate() { + if (DBG) Log.d(TAG, "onCreate()"); + super.onCreate(); + + SynthThread synthThread = new SynthThread(); + synthThread.start(); + mSynthHandler = new SynthHandler(synthThread.getLooper()); + + mCallbacks = new CallbackMap(); + + // Load default language + onLoadLanguage(getDefaultLanguage(), getDefaultCountry(), getDefaultVariant()); + } + + @Override + public void onDestroy() { + if (DBG) Log.d(TAG, "onDestroy()"); + + // Tell the synthesizer to stop + mSynthHandler.quit(); + + // Unregister all callbacks. + mCallbacks.kill(); + + super.onDestroy(); + } + + /** + * Checks whether the engine supports a given language. + * + * Can be called on multiple threads. + * + * @param lang ISO-3 language code. + * @param country ISO-3 country code. May be empty or null. + * @param variant Language variant. May be empty or null. + * @return Code indicating the support status for the locale. + * One of {@link TextToSpeech#LANG_AVAILABLE}, + * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, + * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, + * {@link TextToSpeech#LANG_MISSING_DATA} + * {@link TextToSpeech#LANG_NOT_SUPPORTED}. + */ + protected abstract int onIsLanguageAvailable(String lang, String country, String variant); + + /** + * Returns the language, country and variant currently being used by the TTS engine. + * + * Can be called on multiple threads. + * + * @return A 3-element array, containing language (ISO 3-letter code), + * country (ISO 3-letter code) and variant used by the engine. + * The country and variant may be {@code ""}. If country is empty, then variant must + * be empty too. + * @see Locale#getISO3Language() + * @see Locale#getISO3Country() + * @see Locale#getVariant() + */ + protected abstract String[] onGetLanguage(); + + /** + * Notifies the engine that it should load a speech synthesis language. There is no guarantee + * that this method is always called before the language is used for synthesis. It is merely + * a hint to the engine that it will probably get some synthesis requests for this language + * at some point in the future. + * + * Can be called on multiple threads. + * + * @param lang ISO-3 language code. + * @param country ISO-3 country code. May be empty or null. + * @param variant Language variant. May be empty or null. + * @return Code indicating the support status for the locale. + * One of {@link TextToSpeech#LANG_AVAILABLE}, + * {@link TextToSpeech#LANG_COUNTRY_AVAILABLE}, + * {@link TextToSpeech#LANG_COUNTRY_VAR_AVAILABLE}, + * {@link TextToSpeech#LANG_MISSING_DATA} + * {@link TextToSpeech#LANG_NOT_SUPPORTED}. + */ + protected abstract int onLoadLanguage(String lang, String country, String variant); + + /** + * Notifies the service that it should stop any in-progress speech synthesis. + * This method can be called even if no speech synthesis is currently in progress. + * + * Can be called on multiple threads, but not on the synthesis thread. + */ + protected abstract void onStop(); + + /** + * Tells the service to synthesize speech from the given text. This method should + * block until the synthesis is finished. + * + * Called on the synthesis thread. + * + * @param request The synthesis request. The method should use the methods in the request + * object to communicate the results of the synthesis. + */ + protected abstract void onSynthesizeText(SynthesisRequest request); + + private boolean areDefaultsEnforced() { + return getSecureSettingInt(Settings.Secure.TTS_USE_DEFAULTS, + TextToSpeech.Engine.USE_DEFAULTS) == 1; + } + + private int getDefaultSpeechRate() { + return getSecureSettingInt(Settings.Secure.TTS_DEFAULT_RATE, Engine.DEFAULT_RATE); + } + + private String getDefaultLanguage() { + return getSecureSettingString(Settings.Secure.TTS_DEFAULT_LANG, + Locale.getDefault().getISO3Language()); + } + + private String getDefaultCountry() { + return getSecureSettingString(Settings.Secure.TTS_DEFAULT_COUNTRY, + Locale.getDefault().getISO3Country()); + } + + private String getDefaultVariant() { + return getSecureSettingString(Settings.Secure.TTS_DEFAULT_VARIANT, + Locale.getDefault().getVariant()); + } + + private int getSecureSettingInt(String name, int defaultValue) { + return Settings.Secure.getInt(getContentResolver(), name, defaultValue); + } + + private String getSecureSettingString(String name, String defaultValue) { + String value = Settings.Secure.getString(getContentResolver(), name); + return value != null ? value : defaultValue; + } + + /** + * Synthesizer thread. This thread is used to run {@link SynthHandler}. + */ + private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler { + + private boolean mFirstIdle = true; + + public SynthThread() { + super(SYNTH_THREAD_NAME, android.os.Process.THREAD_PRIORITY_AUDIO); + } + + @Override + protected void onLooperPrepared() { + getLooper().getQueue().addIdleHandler(this); + } + + @Override + public boolean queueIdle() { + if (mFirstIdle) { + mFirstIdle = false; + } else { + broadcastTtsQueueProcessingCompleted(); + } + return true; + } + + private void broadcastTtsQueueProcessingCompleted() { + Intent i = new Intent(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED); + if (DBG) Log.d(TAG, "Broadcasting: " + i); + sendBroadcast(i); + } + } + + private class SynthHandler extends Handler { + + private SpeechItem mCurrentSpeechItem = null; + + public SynthHandler(Looper looper) { + super(looper); + } + + private void dispatchUtteranceCompleted(SpeechItem item) { + String utteranceId = item.getUtteranceId(); + if (!TextUtils.isEmpty(utteranceId)) { + mCallbacks.dispatchUtteranceCompleted(item.getCallingApp(), utteranceId); + } + } + + private synchronized SpeechItem getCurrentSpeechItem() { + return mCurrentSpeechItem; + } + + private synchronized SpeechItem setCurrentSpeechItem(SpeechItem speechItem) { + SpeechItem old = mCurrentSpeechItem; + mCurrentSpeechItem = speechItem; + return old; + } + + public boolean isSpeaking() { + return getCurrentSpeechItem() != null; + } + + public void quit() { + // Don't process any more speech items + getLooper().quit(); + // Stop the current speech item + SpeechItem current = setCurrentSpeechItem(null); + if (current != null) { + current.stop(); + } + } + + /** + * Adds a speech item to the queue. + * + * Called on a service binder thread. + */ + public int enqueueSpeechItem(int queueMode, final SpeechItem speechItem) { + if (!speechItem.isValid()) { + return TextToSpeech.ERROR; + } + // TODO: The old code also supported the undocumented queueMode == 2, + // which clears out all pending items from the calling app, as well as all + // non-file items from other apps. + if (queueMode == TextToSpeech.QUEUE_FLUSH) { + stop(speechItem.getCallingApp()); + } + Runnable runnable = new Runnable() { + @Override + public void run() { + setCurrentSpeechItem(speechItem); + if (speechItem.play() == TextToSpeech.SUCCESS) { + dispatchUtteranceCompleted(speechItem); + } + setCurrentSpeechItem(null); + } + }; + Message msg = Message.obtain(this, runnable); + // The obj is used to remove all callbacks from the given app in stop(String). + msg.obj = speechItem.getCallingApp(); + if (sendMessage(msg)) { + return TextToSpeech.SUCCESS; + } else { + Log.w(TAG, "SynthThread has quit"); + return TextToSpeech.ERROR; + } + } + + /** + * Stops all speech output and removes any utterances still in the queue for + * the calling app. + * + * Called on a service binder thread. + */ + public int stop(String callingApp) { + if (TextUtils.isEmpty(callingApp)) { + return TextToSpeech.ERROR; + } + removeCallbacksAndMessages(callingApp); + SpeechItem current = setCurrentSpeechItem(null); + if (current != null && TextUtils.equals(callingApp, current.getCallingApp())) { + current.stop(); + } + return TextToSpeech.SUCCESS; + } + } + + /** + * An item in the synth thread queue. + */ + private static abstract class SpeechItem { + private final String mCallingApp; + protected final Bundle mParams; + private boolean mStarted = false; + private boolean mStopped = false; + + public SpeechItem(String callingApp, Bundle params) { + mCallingApp = callingApp; + mParams = params; + } + + public String getCallingApp() { + return mCallingApp; + } + + /** + * Checker whether the item is valid. If this method returns false, the item should not + * be played. + */ + public abstract boolean isValid(); + + /** + * Plays the speech item. Blocks until playback is finished. + * Must not be called more than once. + * + * Only called on the synthesis thread. + * + * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + */ + public int play() { + synchronized (this) { + if (mStarted) { + throw new IllegalStateException("play() called twice"); + } + mStarted = true; + } + return playImpl(); + } + + /** + * Stops the speech item. + * Must not be called more than once. + * + * Can be called on multiple threads, but not on the synthesis thread. + */ + public void stop() { + synchronized (this) { + if (mStopped) { + throw new IllegalStateException("stop() called twice"); + } + mStopped = true; + } + stopImpl(); + } + + protected abstract int playImpl(); + + protected abstract void stopImpl(); + + public int getStreamType() { + return getIntParam(Engine.KEY_PARAM_STREAM, Engine.DEFAULT_STREAM); + } + + public float getVolume() { + return getFloatParam(Engine.KEY_PARAM_VOLUME, Engine.DEFAULT_VOLUME); + } + + public float getPan() { + return getFloatParam(Engine.KEY_PARAM_PAN, Engine.DEFAULT_PAN); + } + + public String getUtteranceId() { + return getStringParam(Engine.KEY_PARAM_UTTERANCE_ID, null); + } + + protected String getStringParam(String key, String defaultValue) { + return mParams == null ? defaultValue : mParams.getString(key, defaultValue); + } + + protected int getIntParam(String key, int defaultValue) { + return mParams == null ? defaultValue : mParams.getInt(key, defaultValue); + } + + protected float getFloatParam(String key, float defaultValue) { + return mParams == null ? defaultValue : mParams.getFloat(key, defaultValue); + } + } + + private class SynthesisSpeechItem extends SpeechItem { + private final String mText; + private SynthesisRequest mSynthesisRequest; + + public SynthesisSpeechItem(String callingApp, Bundle params, String text) { + super(callingApp, params); + mText = text; + } + + public String getText() { + return mText; + } + + @Override + public boolean isValid() { + if (TextUtils.isEmpty(mText)) { + Log.w(TAG, "Got empty text"); + return false; + } + if (mText.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH){ + Log.w(TAG, "Text too long: " + mText.length() + " chars"); + return false; + } + return true; + } + + @Override + protected int playImpl() { + SynthesisRequest synthesisRequest; + synchronized (this) { + mSynthesisRequest = createSynthesisRequest(); + synthesisRequest = mSynthesisRequest; + } + setRequestParams(synthesisRequest); + TextToSpeechService.this.onSynthesizeText(synthesisRequest); + return synthesisRequest.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR; + } + + protected SynthesisRequest createSynthesisRequest() { + return new PlaybackSynthesisRequest(mText, mParams, + getStreamType(), getVolume(), getPan()); + } + + private void setRequestParams(SynthesisRequest request) { + if (areDefaultsEnforced()) { + request.setLanguage(getDefaultLanguage(), getDefaultCountry(), getDefaultVariant()); + request.setSpeechRate(getDefaultSpeechRate()); + } else { + request.setLanguage(getLanguage(), getCountry(), getVariant()); + request.setSpeechRate(getSpeechRate()); + } + request.setPitch(getPitch()); + } + + @Override + protected void stopImpl() { + SynthesisRequest synthesisRequest; + synchronized (this) { + synthesisRequest = mSynthesisRequest; + } + synthesisRequest.stop(); + TextToSpeechService.this.onStop(); + } + + public String getLanguage() { + return getStringParam(Engine.KEY_PARAM_LANGUAGE, getDefaultLanguage()); + } + + private boolean hasLanguage() { + return !TextUtils.isEmpty(getStringParam(Engine.KEY_PARAM_LANGUAGE, null)); + } + + private String getCountry() { + if (!hasLanguage()) return getDefaultCountry(); + return getStringParam(Engine.KEY_PARAM_COUNTRY, ""); + } + + private String getVariant() { + if (!hasLanguage()) return getDefaultVariant(); + return getStringParam(Engine.KEY_PARAM_VARIANT, ""); + } + + private int getSpeechRate() { + return getIntParam(Engine.KEY_PARAM_RATE, getDefaultSpeechRate()); + } + + private int getPitch() { + return getIntParam(Engine.KEY_PARAM_PITCH, Engine.DEFAULT_PITCH); + } + } + + private class SynthesisToFileSpeechItem extends SynthesisSpeechItem { + private final File mFile; + + public SynthesisToFileSpeechItem(String callingApp, Bundle params, String text, + File file) { + super(callingApp, params, text); + mFile = file; + } + + @Override + public boolean isValid() { + if (!super.isValid()) { + return false; + } + return checkFile(mFile); + } + + @Override + protected SynthesisRequest createSynthesisRequest() { + return new FileSynthesisRequest(getText(), mParams, mFile); + } + + /** + * Checks that the given file can be used for synthesis output. + */ + private boolean checkFile(File file) { + try { + if (file.exists()) { + Log.v(TAG, "File " + file + " exists, deleting."); + if (!file.delete()) { + Log.e(TAG, "Failed to delete " + file); + return false; + } + } + if (!file.createNewFile()) { + Log.e(TAG, "Can't create file " + file); + return false; + } + if (!file.delete()) { + Log.e(TAG, "Failed to delete " + file); + return false; + } + return true; + } catch (IOException e) { + Log.e(TAG, "Can't use " + file + " due to exception " + e); + return false; + } + } + } + + private class AudioSpeechItem extends SpeechItem { + + private final BlockingMediaPlayer mPlayer; + + public AudioSpeechItem(String callingApp, Bundle params, Uri uri) { + super(callingApp, params); + mPlayer = new BlockingMediaPlayer(TextToSpeechService.this, uri, getStreamType()); + } + + @Override + public boolean isValid() { + return true; + } + + @Override + protected int playImpl() { + return mPlayer.startAndWait() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR; + } + + @Override + protected void stopImpl() { + mPlayer.stop(); + } + } + + private class SilenceSpeechItem extends SpeechItem { + private final long mDuration; + private final ConditionVariable mDone; + + public SilenceSpeechItem(String callingApp, Bundle params, long duration) { + super(callingApp, params); + mDuration = duration; + mDone = new ConditionVariable(); + } + + @Override + public boolean isValid() { + return true; + } + + @Override + protected int playImpl() { + boolean aborted = mDone.block(mDuration); + return aborted ? TextToSpeech.ERROR : TextToSpeech.SUCCESS; + } + + @Override + protected void stopImpl() { + mDone.open(); + } + } + + @Override + public IBinder onBind(Intent intent) { + if (TextToSpeech.Engine.INTENT_ACTION_TTS_SERVICE.equals(intent.getAction())) { + return mBinder; + } + return null; + } + + /** + * Binder returned from {@code #onBind(Intent)}. The methods in this class can be + * called called from several different threads. + */ + private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { + + public int speak(String callingApp, String text, int queueMode, Bundle params) { + SpeechItem item = new SynthesisSpeechItem(callingApp, params, text); + return mSynthHandler.enqueueSpeechItem(queueMode, item); + } + + public int synthesizeToFile(String callingApp, String text, String filename, + Bundle params) { + File file = new File(filename); + SpeechItem item = new SynthesisToFileSpeechItem(callingApp, params, text, file); + return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); + } + + public int playAudio(String callingApp, Uri audioUri, int queueMode, Bundle params) { + SpeechItem item = new AudioSpeechItem(callingApp, params, audioUri); + return mSynthHandler.enqueueSpeechItem(queueMode, item); + } + + public int playSilence(String callingApp, long duration, int queueMode, Bundle params) { + SpeechItem item = new SilenceSpeechItem(callingApp, params, duration); + return mSynthHandler.enqueueSpeechItem(queueMode, item); + } + + public boolean isSpeaking() { + return mSynthHandler.isSpeaking(); + } + + public int stop(String callingApp) { + return mSynthHandler.stop(callingApp); + } + + public String[] getLanguage() { + return onGetLanguage(); + } + + public int isLanguageAvailable(String lang, String country, String variant) { + return onIsLanguageAvailable(lang, country, variant); + } + + public int loadLanguage(String lang, String country, String variant) { + return onLoadLanguage(lang, country, variant); + } + + public void setCallback(String packageName, ITextToSpeechCallback cb) { + mCallbacks.setCallback(packageName, cb); + } + }; + + private class CallbackMap extends RemoteCallbackList<ITextToSpeechCallback> { + + private final HashMap<String, ITextToSpeechCallback> mAppToCallback + = new HashMap<String, ITextToSpeechCallback>(); + + public void setCallback(String packageName, ITextToSpeechCallback cb) { + synchronized (mAppToCallback) { + ITextToSpeechCallback old; + if (cb != null) { + register(cb, packageName); + old = mAppToCallback.put(packageName, cb); + } else { + old = mAppToCallback.remove(packageName); + } + if (old != null && old != cb) { + unregister(old); + } + } + } + + public void dispatchUtteranceCompleted(String packageName, String utteranceId) { + ITextToSpeechCallback cb; + synchronized (mAppToCallback) { + cb = mAppToCallback.get(packageName); + } + if (cb == null) return; + try { + cb.utteranceCompleted(utteranceId); + } catch (RemoteException e) { + Log.e(TAG, "Callback failed: " + e); + } + } + + @Override + public void onCallbackDied(ITextToSpeechCallback callback, Object cookie) { + String packageName = (String) cookie; + synchronized (mAppToCallback) { + mAppToCallback.remove(packageName); + } + mSynthHandler.stop(packageName); + } + + @Override + public void kill() { + synchronized (mAppToCallback) { + mAppToCallback.clear(); + super.kill(); + } + } + + } + +} diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 9309b05..757a8c3 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -234,18 +234,17 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback * provided Metrics object (or a new one if the provided one was null) * if boring. */ - public static Metrics isBoring(CharSequence text, TextPaint paint, - Metrics metrics) { + public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) { char[] temp = TextUtils.obtain(500); - int len = text.length(); + int length = text.length(); boolean boring = true; outer: - for (int i = 0; i < len; i += 500) { + for (int i = 0; i < length; i += 500) { int j = i + 500; - if (j > len) - j = len; + if (j > length) + j = length; TextUtils.getChars(text, i, j, temp, 0); @@ -265,7 +264,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback if (boring && text instanceof Spanned) { Spanned sp = (Spanned) text; - Object[] styles = sp.getSpans(0, text.length(), ParagraphStyle.class); + Object[] styles = sp.getSpans(0, length, ParagraphStyle.class); if (styles.length > 0) { boring = false; } @@ -278,7 +277,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback } TextLine line = TextLine.obtain(); - line.set(paint, text, 0, text.length(), Layout.DIR_LEFT_TO_RIGHT, + line.set(paint, text, 0, length, Layout.DIR_LEFT_TO_RIGHT, Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); fm.width = (int) FloatMath.ceil(line.metrics(fm)); TextLine.recycle(line); @@ -289,52 +288,63 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback } } - @Override public int getHeight() { + @Override + public int getHeight() { return mBottom; } - @Override public int getLineCount() { + @Override + public int getLineCount() { return 1; } - @Override public int getLineTop(int line) { + @Override + public int getLineTop(int line) { if (line == 0) return 0; else return mBottom; } - @Override public int getLineDescent(int line) { + @Override + public int getLineDescent(int line) { return mDesc; } - @Override public int getLineStart(int line) { + @Override + public int getLineStart(int line) { if (line == 0) return 0; else return getText().length(); } - @Override public int getParagraphDirection(int line) { + @Override + public int getParagraphDirection(int line) { return DIR_LEFT_TO_RIGHT; } - @Override public boolean getLineContainsTab(int line) { + @Override + public boolean getLineContainsTab(int line) { return false; } - @Override public float getLineMax(int line) { + @Override + public float getLineMax(int line) { return mMax; } - @Override public final Directions getLineDirections(int line) { + @Override + public final Directions getLineDirections(int line) { return Layout.DIRS_ALL_LEFT_TO_RIGHT; } + @Override public int getTopPadding() { return mTopPadding; } + @Override public int getBottomPadding() { return mBottomPadding; } diff --git a/core/java/android/text/CharSequenceIterator.java b/core/java/android/text/CharSequenceIterator.java new file mode 100644 index 0000000..4b8ac10 --- /dev/null +++ b/core/java/android/text/CharSequenceIterator.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import java.text.CharacterIterator; + +/** {@hide} */ +public class CharSequenceIterator implements CharacterIterator { + private final CharSequence mValue; + + private final int mLength; + private int mIndex; + + public CharSequenceIterator(CharSequence value) { + mValue = value; + mLength = value.length(); + mIndex = 0; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); + } + } + + /** {@inheritDoc} */ + public char current() { + if (mIndex == mLength) { + return DONE; + } + return mValue.charAt(mIndex); + } + + /** {@inheritDoc} */ + public int getBeginIndex() { + return 0; + } + + /** {@inheritDoc} */ + public int getEndIndex() { + return mLength; + } + + /** {@inheritDoc} */ + public int getIndex() { + return mIndex; + } + + /** {@inheritDoc} */ + public char first() { + return setIndex(0); + } + + /** {@inheritDoc} */ + public char last() { + return setIndex(mLength - 1); + } + + /** {@inheritDoc} */ + public char next() { + if (mIndex == mLength) { + return DONE; + } + return setIndex(mIndex + 1); + } + + /** {@inheritDoc} */ + public char previous() { + if (mIndex == 0) { + return DONE; + } + return setIndex(mIndex - 1); + } + + /** {@inheritDoc} */ + public char setIndex(int index) { + if ((index < 0) || (index > mLength)) { + throw new IllegalArgumentException("Valid range is [" + 0 + "..." + mLength + "]"); + } + mIndex = index; + return current(); + } +} diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java index d426d12..831ccc5 100644 --- a/core/java/android/text/GraphicsOperations.java +++ b/core/java/android/text/GraphicsOperations.java @@ -58,6 +58,13 @@ extends CharSequence int flags, float[] advances, int advancesIndex, Paint paint); /** + * Just like {@link Paint#getTextRunAdvances}. + * @hide + */ + float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, + int flags, float[] advances, int advancesIndex, Paint paint, int reserved); + + /** * Just like {@link Paint#getTextRunCursor}. * @hide */ diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java index 13cb5e6..679e2cc 100644 --- a/core/java/android/text/Selection.java +++ b/core/java/android/text/Selection.java @@ -16,6 +16,8 @@ package android.text; +import java.text.BreakIterator; + /** * Utility class for manipulating cursors and selections in CharSequences. @@ -38,7 +40,7 @@ public class Selection { else return -1; } - + /** * Return the offset of the selection edge or cursor, or -1 if * there is no selection or cursor. @@ -57,7 +59,7 @@ public class Selection { // private static int pin(int value, int min, int max) { // return value < min ? 0 : (value > max ? max : value); // } - + /** * Set the selection anchor to <code>start</code> and the selection edge * to <code>stop</code>. @@ -69,7 +71,7 @@ public class Selection { int ostart = getSelectionStart(text); int oend = getSelectionEnd(text); - + if (ostart != start || oend != stop) { text.setSpan(SELECTION_START, start, start, Spanned.SPAN_POINT_POINT|Spanned.SPAN_INTERMEDIATE); @@ -357,6 +359,42 @@ public class Selection { return true; } + /** {@hide} */ + public static interface PositionIterator { + public static final int DONE = BreakIterator.DONE; + + public int preceding(int position); + public int following(int position); + } + + /** {@hide} */ + public static boolean moveToPreceding( + Spannable text, PositionIterator iter, boolean extendSelection) { + final int offset = iter.preceding(getSelectionEnd(text)); + if (offset != PositionIterator.DONE) { + if (extendSelection) { + extendSelection(text, offset); + } else { + setSelection(text, offset); + } + } + return true; + } + + /** {@hide} */ + public static boolean moveToFollowing( + Spannable text, PositionIterator iter, boolean extendSelection) { + final int offset = iter.following(getSelectionEnd(text)); + if (offset != PositionIterator.DONE) { + if (extendSelection) { + extendSelection(text, offset); + } else { + setSelection(text, offset); + } + } + return true; + } + private static int findEdge(Spannable text, Layout layout, int dir) { int pt = getSelectionEnd(text); int line = layout.getLineForOffset(pt); @@ -419,7 +457,7 @@ public class Selection { private static final class START implements NoCopySpan { } private static final class END implements NoCopySpan { } - + /* * Public constants */ diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index ea5cdfe..6bde802 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -20,6 +20,7 @@ import com.android.internal.util.ArrayUtils; import android.graphics.Canvas; import android.graphics.Paint; +import android.text.style.SuggestionSpan; import java.lang.reflect.Array; @@ -277,8 +278,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, TextWatcher[] recipients = null; if (notify) - recipients = sendTextWillChange(start, end - start, - tbend - tbstart); + recipients = sendTextWillChange(start, end - start, tbend - tbstart); for (int i = mSpanCount - 1; i >= 0; i--) { if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) { @@ -353,6 +353,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, // no need for span fixup on pure insertion if (tbend > tbstart && end - start == 0) { if (notify) { + removeSuggestionSpans(start, end); sendTextChange(recipients, start, end - start, tbend - tbstart); sendTextHasChanged(recipients); } @@ -384,20 +385,10 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } // remove 0-length SPAN_EXCLUSIVE_EXCLUSIVE - // XXX send notification on removal - if (mSpanEnds[i] < mSpanStarts[i]) { - System.arraycopy(mSpans, i + 1, - mSpans, i, mSpanCount - (i + 1)); - System.arraycopy(mSpanStarts, i + 1, - mSpanStarts, i, mSpanCount - (i + 1)); - System.arraycopy(mSpanEnds, i + 1, - mSpanEnds, i, mSpanCount - (i + 1)); - System.arraycopy(mSpanFlags, i + 1, - mSpanFlags, i, mSpanCount - (i + 1)); - - mSpanCount--; + removeSpan(i); } + removeSuggestionSpans(start, end); } if (notify) { @@ -408,6 +399,32 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, return ret; } + /** + * Removes the SuggestionSpan that overlap the [start, end] range, and that would + * not make sense anymore after the change. + */ + private void removeSuggestionSpans(int start, int end) { + for (int i = mSpanCount - 1; i >= 0; i--) { + final int spanEnd = mSpanEnds[i]; + final int spanSpart = mSpanStarts[i]; + if ((mSpans[i] instanceof SuggestionSpan) && ( + (spanSpart < start && spanEnd > start) || + (spanSpart < end && spanEnd > end))) { + removeSpan(i); + } + } + } + + private void removeSpan(int i) { + // XXX send notification on removal + System.arraycopy(mSpans, i + 1, mSpans, i, mSpanCount - (i + 1)); + System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, mSpanCount - (i + 1)); + System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, mSpanCount - (i + 1)); + System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, mSpanCount - (i + 1)); + + mSpanCount--; + } + // Documentation from interface public SpannableStringBuilder replace(int start, int end, CharSequence tb) { return replace(start, end, tb, 0, tb.length()); @@ -465,16 +482,15 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, mGapStart++; mGapLength--; - if (mGapLength < 1) + if (mGapLength < 1) { new Exception("mGapLength < 1").printStackTrace(); + } int oldlen = (end + 1) - start; - int inserted = change(false, start + 1, start + 1, - tb, tbstart, tbend); + int inserted = change(false, start + 1, start + 1, tb, tbstart, tbend); change(false, start, start + 1, "", 0, 0); - change(false, start + inserted, start + inserted + oldlen - 1, - "", 0, 0); + change(false, start + inserted, start + inserted + oldlen - 1, "", 0, 0); /* * Special case to keep the cursor in the same position @@ -1170,6 +1186,35 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } /** + * Don't call this yourself -- exists for Paint to use internally. + * {@hide} + */ + public float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, int flags, + float[] advances, int advancesPos, Paint p, int reserved) { + + float ret; + + int contextLen = contextEnd - contextStart; + int len = end - start; + + if (end <= mGapStart) { + ret = p.getTextRunAdvances(mText, start, len, contextStart, contextLen, + flags, advances, advancesPos, reserved); + } else if (start >= mGapStart) { + ret = p.getTextRunAdvances(mText, start + mGapLength, len, + contextStart + mGapLength, contextLen, flags, advances, advancesPos, reserved); + } else { + char[] buf = TextUtils.obtain(contextLen); + getChars(contextStart, contextEnd, buf, 0); + ret = p.getTextRunAdvances(buf, start - contextStart, len, + 0, contextLen, flags, advances, advancesPos, reserved); + TextUtils.recycle(buf); + } + + return ret; + } + + /** * Returns the next cursor position in the run. This avoids placing the cursor between * surrogates, between characters that form conjuncts, between base characters and combining * marks, or within a reordering cluster. @@ -1245,7 +1290,6 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, private int[] mSpanFlags; private int mSpanCount; - private static final int MARK = 1; private static final int POINT = 2; private static final int PARAGRAPH = 3; diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index a826a97..9e48eff 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -235,6 +235,8 @@ public class StaticLayout extends Layout { } else { MetricAffectingSpan[] spans = spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, spanned, + MetricAffectingSpan.class); measured.addStyleRun(paint, spans, spanLen, fm); } } diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 90279d1..0f8097a 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -127,12 +127,12 @@ class TextLine { boolean hasReplacement = false; if (text instanceof Spanned) { mSpanned = (Spanned) text; - hasReplacement = mSpanned.getSpans(start, limit, - ReplacementSpan.class).length > 0; + ReplacementSpan[] spans = mSpanned.getSpans(start, limit, ReplacementSpan.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, ReplacementSpan.class); + hasReplacement = spans.length > 0; } - mCharsValid = hasReplacement || hasTabs || - directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; + mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT; if (mCharsValid) { if (mChars == null || mChars.length < mLen) { @@ -147,10 +147,11 @@ class TextLine { // zero-width characters. char[] chars = mChars; for (int i = start, inext; i < limit; i = inext) { - inext = mSpanned.nextSpanTransition(i, limit, - ReplacementSpan.class); - if (mSpanned.getSpans(i, inext, ReplacementSpan.class) - .length > 0) { // transition into a span + inext = mSpanned.nextSpanTransition(i, limit, ReplacementSpan.class); + ReplacementSpan[] spans = mSpanned.getSpans(i, inext, ReplacementSpan.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, ReplacementSpan.class); + if (spans.length > 0) { + // transition into a span chars[i - start] = '\ufffc'; for (int j = i - start + 1, e = inext - start; j < e; ++j) { chars[j] = '\ufeff'; // used as ZWNBS, marks positions to skip @@ -197,7 +198,6 @@ class TextLine { boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; int segstart = runStart; - char[] chars = mChars; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { int codept = 0; Bitmap bm = null; @@ -629,6 +629,7 @@ class TextLine { MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + spanStart, mStart + spanLimit, MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); if (spans.length > 0) { ReplacementSpan replacement = null; @@ -814,6 +815,13 @@ class TextLine { int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth) { + // Case of an empty line, make sure we update fmi according to mPaint + if (start == measureLimit) { + TextPaint wp = mWorkPaint; + wp.set(mPaint); + return handleText(wp, 0, 0, 0, 0, runIsRtl, c, x, top, y, bottom, fmi, needWidth); + } + // Shaping needs to take into account context up to metric boundaries, // but rendering needs to take into account character style boundaries. // So we iterate through metric runs to get metric bounds, @@ -835,6 +843,7 @@ class TextLine { mlimit = inext < measureLimit ? inext : measureLimit; MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i, mStart + mlimit, MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class); if (spans.length > 0) { ReplacementSpan replacement = null; @@ -868,6 +877,7 @@ class TextLine { CharacterStyle[] spans = mSpanned.getSpans(mStart + j, mStart + jnext, CharacterStyle.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, CharacterStyle.class); wp.set(mPaint); for (int k = 0; k < spans.length; k++) { diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index cdb7228..6741059 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -37,6 +37,7 @@ import android.text.style.ScaleXSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.SubscriptSpan; +import android.text.style.SuggestionSpan; import android.text.style.SuperscriptSpan; import android.text.style.TextAppearanceSpan; import android.text.style.TypefaceSpan; @@ -44,6 +45,7 @@ import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Printer; +import java.lang.reflect.Array; import java.util.Iterator; import java.util.regex.Pattern; @@ -54,7 +56,7 @@ public class TextUtils { public static void getChars(CharSequence s, int start, int end, char[] dest, int destoff) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (c == String.class) ((String) s).getChars(start, end, dest, destoff); @@ -75,7 +77,7 @@ public class TextUtils { } public static int indexOf(CharSequence s, char ch, int start) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (c == String.class) return ((String) s).indexOf(ch, start); @@ -84,7 +86,7 @@ public class TextUtils { } public static int indexOf(CharSequence s, char ch, int start, int end) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (s instanceof GetChars || c == StringBuffer.class || c == StringBuilder.class || c == String.class) { @@ -125,7 +127,7 @@ public class TextUtils { } public static int lastIndexOf(CharSequence s, char ch, int last) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (c == String.class) return ((String) s).lastIndexOf(ch, last); @@ -142,7 +144,7 @@ public class TextUtils { int end = last + 1; - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (s instanceof GetChars || c == StringBuffer.class || c == StringBuilder.class || c == String.class) { @@ -499,6 +501,7 @@ public class TextUtils { return new String(buf); } + @Override public String toString() { return subSequence(0, length()).toString(); } @@ -563,6 +566,8 @@ public class TextUtils { public static final int TEXT_APPEARANCE_SPAN = 17; /** @hide */ public static final int ANNOTATION = 18; + /** @hide */ + public static final int SUGGESTION_SPAN = 19; /** * Flatten a CharSequence and whatever styles can be copied across processes @@ -621,7 +626,7 @@ public class TextUtils { * Read and return a new CharSequence, possibly with styles, * from the parcel. */ - public CharSequence createFromParcel(Parcel p) { + public CharSequence createFromParcel(Parcel p) { int kind = p.readInt(); String string = p.readString(); @@ -714,6 +719,10 @@ public class TextUtils { readSpan(p, sp, new Annotation(p)); break; + case SUGGESTION_SPAN: + readSpan(p, sp, new SuggestionSpan(p)); + break; + default: throw new RuntimeException("bogus span encoding " + kind); } @@ -766,7 +775,7 @@ public class TextUtils { if (where >= 0) tb.setSpan(sources[i], where, where + sources[i].length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } for (int i = 0; i < sources.length; i++) { @@ -1120,7 +1129,6 @@ public class TextUtils { int remaining = commaCount + 1; int ok = 0; - int okRemaining = remaining; String okFormat = ""; int w = 0; @@ -1152,7 +1160,6 @@ public class TextUtils { if (w + moreWid <= avail) { ok = i + 1; - okRemaining = remaining; okFormat = format; } } @@ -1185,6 +1192,7 @@ public class TextUtils { MetricAffectingSpan.class); MetricAffectingSpan[] spans = sp.getSpans( spanStart, spanEnd, MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class); width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null); } } @@ -1543,6 +1551,56 @@ public class TextUtils { return false; } + /** + * Removes empty spans from the <code>spans</code> array. + * + * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans + * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by + * one of these transitions will (correctly) include the empty overlapping span. + * + * However, these empty spans should not be taken into account when layouting or rendering the + * string and this method provides a way to filter getSpans' results accordingly. + * + * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from + * the <code>spanned</code> + * @param spanned The Spanned from which spans were extracted + * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} == + * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved + * @hide + */ + @SuppressWarnings("unchecked") + public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) { + T[] copy = null; + int count = 0; + + for (int i = 0; i < spans.length; i++) { + final T span = spans[i]; + final int start = spanned.getSpanStart(span); + final int end = spanned.getSpanEnd(span); + + if (start == end) { + if (copy == null) { + copy = (T[]) Array.newInstance(klass, spans.length - 1); + System.arraycopy(spans, 0, copy, 0, i); + count = i; + } + } else { + if (copy != null) { + copy[count] = span; + count++; + } + } + } + + if (copy != null) { + T[] result = (T[]) Array.newInstance(klass, count); + System.arraycopy(copy, 0, result, 0, count); + return result; + } else { + return spans; + } + } + private static Object sLock = new Object(); private static char[] sTemp = null; } diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index a61ff13..d432dee 100644 --- a/core/java/android/text/method/ArrowKeyMovementMethod.java +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -193,6 +193,20 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme } } + /** {@hide} */ + @Override + protected boolean leftWord(TextView widget, Spannable buffer) { + mWordIterator.setCharSequence(buffer); + return Selection.moveToPreceding(buffer, mWordIterator, isSelecting(buffer)); + } + + /** {@hide} */ + @Override + protected boolean rightWord(TextView widget, Spannable buffer) { + mWordIterator.setCharSequence(buffer); + return Selection.moveToFollowing(buffer, mWordIterator, isSelecting(buffer)); + } + @Override protected boolean home(TextView widget, Spannable buffer) { return lineStart(widget, buffer); @@ -205,7 +219,8 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { - int initialScrollX = -1, initialScrollY = -1; + int initialScrollX = -1; + int initialScrollY = -1; final int action = event.getAction(); if (action == MotionEvent.ACTION_UP) { @@ -220,7 +235,7 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme boolean cap = isSelecting(buffer); if (cap) { int offset = widget.getOffset((int) event.getX(), (int) event.getY()); - + buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT); // Disallow intercepting of the touch events, so that @@ -308,6 +323,7 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme return sInstance; } + private WordIterator mWordIterator = new WordIterator(); private static final Object LAST_TAP_DOWN = new Object(); private static ArrowKeyMovementMethod sInstance; diff --git a/core/java/android/text/method/BaseMovementMethod.java b/core/java/android/text/method/BaseMovementMethod.java index 94c6ed0..f554b90 100644 --- a/core/java/android/text/method/BaseMovementMethod.java +++ b/core/java/android/text/method/BaseMovementMethod.java @@ -164,6 +164,9 @@ public class BaseMovementMethod implements MovementMethod { if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return left(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_CTRL_ON)) { + return leftWord(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_ALT_ON)) { return lineStart(widget, buffer); } @@ -173,6 +176,9 @@ public class BaseMovementMethod implements MovementMethod { if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return right(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_CTRL_ON)) { + return rightWord(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_ALT_ON)) { return lineEnd(widget, buffer); } @@ -217,12 +223,18 @@ public class BaseMovementMethod implements MovementMethod { case KeyEvent.KEYCODE_MOVE_HOME: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return home(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_CTRL_ON)) { + return top(widget, buffer); } break; case KeyEvent.KEYCODE_MOVE_END: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return end(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_CTRL_ON)) { + return bottom(widget, buffer); } break; } @@ -349,6 +361,16 @@ public class BaseMovementMethod implements MovementMethod { return false; } + /** {@hide} */ + protected boolean leftWord(TextView widget, Spannable buffer) { + return false; + } + + /** {@hide} */ + protected boolean rightWord(TextView widget, Spannable buffer) { + return false; + } + /** * Performs a home movement action. * Moves the cursor or scrolls to the start of the line or to the top of the diff --git a/core/java/android/text/method/WordIterator.java b/core/java/android/text/method/WordIterator.java new file mode 100644 index 0000000..b250414 --- /dev/null +++ b/core/java/android/text/method/WordIterator.java @@ -0,0 +1,220 @@ + +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text.method; + +import android.text.CharSequenceIterator; +import android.text.Editable; +import android.text.Selection; +import android.text.Spanned; +import android.text.TextWatcher; + +import java.text.BreakIterator; +import java.text.CharacterIterator; +import java.util.Locale; + +/** + * Walks through cursor positions at word boundaries. Internally uses + * {@link BreakIterator#getWordInstance()}, and caches {@link CharSequence} + * for performance reasons. + * + * Also provides methods to determine word boundaries. + * {@hide} + */ +public class WordIterator implements Selection.PositionIterator { + private CharSequence mCurrent; + private boolean mCurrentDirty = false; + + private BreakIterator mIterator; + + /** + * Constructs a WordIterator using the default locale. + */ + public WordIterator() { + this(Locale.getDefault()); + } + + /** + * Constructs a new WordIterator for the specified locale. + * @param locale The locale to be used when analysing the text. + */ + public WordIterator(Locale locale) { + mIterator = BreakIterator.getWordInstance(locale); + } + + private final TextWatcher mWatcher = new TextWatcher() { + /** {@inheritDoc} */ + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // ignored + } + + /** {@inheritDoc} */ + public void onTextChanged(CharSequence s, int start, int before, int count) { + mCurrentDirty = true; + } + + /** {@inheritDoc} */ + public void afterTextChanged(Editable s) { + // ignored + } + }; + + public void setCharSequence(CharSequence incoming) { + // When incoming is different object, move listeners to new sequence + // and mark as dirty so we reload contents. + if (mCurrent != incoming) { + if (mCurrent instanceof Editable) { + ((Editable) mCurrent).removeSpan(mWatcher); + } + + if (incoming instanceof Editable) { + ((Editable) incoming).setSpan( + mWatcher, 0, incoming.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + mCurrent = incoming; + mCurrentDirty = true; + } + + if (mCurrentDirty) { + final CharacterIterator charIterator = new CharSequenceIterator(mCurrent); + mIterator.setText(charIterator); + + mCurrentDirty = false; + } + } + + /** {@inheritDoc} */ + public int preceding(int offset) { + do { + offset = mIterator.preceding(offset); + if (offset == BreakIterator.DONE || isOnLetterOrDigit(offset)) { + break; + } + } while (true); + + return offset; + } + + /** {@inheritDoc} */ + public int following(int offset) { + do { + offset = mIterator.following(offset); + if (offset == BreakIterator.DONE || isAfterLetterOrDigit(offset)) { + break; + } + } while (true); + + return offset; + } + + /** If <code>offset</code> is within a word, returns the index of the first character of that + * word, otherwise returns BreakIterator.DONE. + * + * The offsets that are considered to be part of a word are the indexes of its characters, + * <i>as well as</i> the index of its last character plus one. + * If offset is the index of a low surrogate character, BreakIterator.DONE will be returned. + * + * Valid range for offset is [0..textLength] (note the inclusive upper bound). + * The returned value is within [0..offset] or BreakIterator.DONE. + * + * @throws IllegalArgumentException is offset is not valid. + */ + public int getBeginning(int offset) { + checkOffsetIsValid(offset); + + if (isOnLetterOrDigit(offset)) { + if (mIterator.isBoundary(offset)) { + return offset; + } else { + return mIterator.preceding(offset); + } + } else { + if (isAfterLetterOrDigit(offset)) { + return mIterator.preceding(offset); + } + } + return BreakIterator.DONE; + } + + /** If <code>offset</code> is within a word, returns the index of the last character of that + * word plus one, otherwise returns BreakIterator.DONE. + * + * The offsets that are considered to be part of a word are the indexes of its characters, + * <i>as well as</i> the index of its last character plus one. + * If offset is the index of a low surrogate character, BreakIterator.DONE will be returned. + * + * Valid range for offset is [0..textLength] (note the inclusive upper bound). + * The returned value is within [offset..textLength] or BreakIterator.DONE. + * + * @throws IllegalArgumentException is offset is not valid. + */ + public int getEnd(int offset) { + checkOffsetIsValid(offset); + + if (isAfterLetterOrDigit(offset)) { + if (mIterator.isBoundary(offset)) { + return offset; + } else { + return mIterator.following(offset); + } + } else { + if (isOnLetterOrDigit(offset)) { + return mIterator.following(offset); + } + } + return BreakIterator.DONE; + } + + private boolean isAfterLetterOrDigit(int offset) { + if (offset - 1 >= 0) { + final char previousChar = mCurrent.charAt(offset - 1); + if (Character.isLetterOrDigit(previousChar)) return true; + if (offset - 2 >= 0) { + final char previousPreviousChar = mCurrent.charAt(offset - 2); + if (Character.isSurrogatePair(previousPreviousChar, previousChar)) { + final int codePoint = Character.toCodePoint(previousPreviousChar, previousChar); + return Character.isLetterOrDigit(codePoint); + } + } + } + return false; + } + + private boolean isOnLetterOrDigit(int offset) { + final int length = mCurrent.length(); + if (offset < length) { + final char currentChar = mCurrent.charAt(offset); + if (Character.isLetterOrDigit(currentChar)) return true; + if (offset + 1 < length) { + final char nextChar = mCurrent.charAt(offset + 1); + if (Character.isSurrogatePair(currentChar, nextChar)) { + final int codePoint = Character.toCodePoint(currentChar, nextChar); + return Character.isLetterOrDigit(codePoint); + } + } + } + return false; + } + + private void checkOffsetIsValid(int offset) { + if (offset < 0 || offset > mCurrent.length()) { + final String message = "Valid range is [0, " + mCurrent.length() + "]"; + throw new IllegalArgumentException(message); + } + } +} diff --git a/core/java/android/text/style/SuggestionSpan.aidl b/core/java/android/text/style/SuggestionSpan.aidl new file mode 100644 index 0000000..3c56cfe --- /dev/null +++ b/core/java/android/text/style/SuggestionSpan.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text.style; + +parcelable SuggestionSpan; diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java new file mode 100644 index 0000000..dcb0898 --- /dev/null +++ b/core/java/android/text/style/SuggestionSpan.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text.style; + +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.ParcelableSpan; +import android.text.TextUtils; + +import java.util.Arrays; +import java.util.Locale; + +/** + * Holds suggestion candidates of words under this span. + */ +public class SuggestionSpan implements ParcelableSpan { + + /** + * Flag for indicating that the input is verbatim. TextView refers to this flag to determine + * how it displays a word with SuggestionSpan. + */ + public static final int FLAG_VERBATIM = 0x0001; + + private static final int SUGGESTIONS_MAX_SIZE = 5; + + /* + * TODO: Needs to check the validity and add a feature that TextView will change + * the current IME to the other IME which is specified in SuggestionSpan. + * An IME needs to set the span by specifying the target IME and Subtype of SuggestionSpan. + * And the current IME might want to specify any IME as the target IME including other IMEs. + */ + + private final int mFlags; + private final String[] mSuggestions; + private final String mLocaleString; + private final String mOriginalString; + /* + * TODO: If switching IME is required, needs to add parameters for ids of InputMethodInfo + * and InputMethodSubtype. + */ + + /** + * @param context Context for the application + * @param suggestions Suggestions for the string under the span + * @param flags Additional flags indicating how this span is handled in TextView + */ + public SuggestionSpan(Context context, String[] suggestions, int flags) { + this(context, null, suggestions, flags, null); + } + + /** + * @param locale Locale of the suggestions + * @param suggestions Suggestions for the string under the span + * @param flags Additional flags indicating how this span is handled in TextView + */ + public SuggestionSpan(Locale locale, String[] suggestions, int flags) { + this(null, locale, suggestions, flags, null); + } + + /** + * @param context Context for the application + * @param locale locale Locale of the suggestions + * @param suggestions Suggestions for the string under the span + * @param flags Additional flags indicating how this span is handled in TextView + * @param originalString originalString for suggestions + */ + public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, + String originalString) { + final int N = Math.min(SUGGESTIONS_MAX_SIZE, suggestions.length); + mSuggestions = Arrays.copyOf(suggestions, N); + mFlags = flags; + if (context != null && locale == null) { + mLocaleString = context.getResources().getConfiguration().locale.toString(); + } else { + mLocaleString = locale.toString(); + } + mOriginalString = originalString; + } + + public SuggestionSpan(Parcel src) { + mSuggestions = src.readStringArray(); + mFlags = src.readInt(); + mLocaleString = src.readString(); + mOriginalString = src.readString(); + } + + /** + * @return suggestions + */ + public String[] getSuggestions() { + return mSuggestions; + } + + /** + * @return locale of suggestions + */ + public String getLocale() { + return mLocaleString; + } + + /** + * @return original string of suggestions + */ + public String getOriginalString() { + return mOriginalString; + } + + public int getFlags() { + return mFlags; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeStringArray(mSuggestions); + dest.writeInt(mFlags); + dest.writeString(mLocaleString); + dest.writeString(mOriginalString); + } + + @Override + public int getSpanTypeId() { + return TextUtils.SUGGESTION_SPAN; + } + + public static final Parcelable.Creator<SuggestionSpan> CREATOR = + new Parcelable.Creator<SuggestionSpan>() { + @Override + public SuggestionSpan createFromParcel(Parcel source) { + return new SuggestionSpan(source); + } + + @Override + public SuggestionSpan[] newArray(int size) { + return new SuggestionSpan[size]; + } + }; +} diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java index de929e3..deed713 100644 --- a/core/java/android/text/style/TextAppearanceSpan.java +++ b/core/java/android/text/style/TextAppearanceSpan.java @@ -51,10 +51,9 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl * to determine the color. The <code>appearance</code> should be, * for example, <code>android.R.style.TextAppearance_Small</code>, * and the <code>colorList</code> should be, for example, - * <code>android.R.styleable.Theme_textColorDim</code>. + * <code>android.R.styleable.Theme_textColorPrimary</code>. */ - public TextAppearanceSpan(Context context, int appearance, - int colorList) { + public TextAppearanceSpan(Context context, int appearance, int colorList) { ColorStateList textColor; TypedArray a = diff --git a/core/java/android/util/Config.java b/core/java/android/util/Config.java deleted file mode 100644 index becb882..0000000 --- a/core/java/android/util/Config.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.util; - -/** - * Build configuration. The constants in this class vary depending - * on release vs. debug build. - * {@more} - */ -public final class Config { - /** @hide */ public Config() {} - - /** - * If this is a debug build, this field will be true. - */ - public static final boolean DEBUG = ConfigBuildFlags.DEBUG; - - /* - * Deprecated fields - * TODO: Remove platform references to these and @hide them. - */ - - /** - * @deprecated Use {@link #DEBUG} instead. - */ - @Deprecated - public static final boolean RELEASE = !DEBUG; - - /** - * @deprecated Always false. - */ - @Deprecated - public static final boolean PROFILE = false; - - /** - * @deprecated Always false. - */ - @Deprecated - public static final boolean LOGV = false; - - /** - * @deprecated Always true. - */ - @Deprecated - public static final boolean LOGD = true; -} diff --git a/core/java/android/util/JsonReader.java b/core/java/android/util/JsonReader.java index 8f44895..132b595 100644 --- a/core/java/android/util/JsonReader.java +++ b/core/java/android/util/JsonReader.java @@ -16,12 +16,13 @@ package android.util; +import java.io.Closeable; import java.io.EOFException; import java.io.IOException; import java.io.Reader; -import java.io.Closeable; import java.util.ArrayList; import java.util.List; +import libcore.internal.StringPool; /** * Reads a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>) @@ -86,7 +87,11 @@ import java.util.List; * * public List<Message> readJsonStream(InputStream in) throws IOException { * JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8")); - * return readMessagesArray(reader); + * try { + * return readMessagesArray(reader); + * } finally { + * reader.close(); + * } * } * * public List<Message> readMessagesArray(JsonReader reader) throws IOException { @@ -173,6 +178,8 @@ public final class JsonReader implements Closeable { private static final String TRUE = "true"; private static final String FALSE = "false"; + private final StringPool stringPool = new StringPool(); + /** The input JSON. */ private final Reader in; @@ -832,7 +839,7 @@ public final class JsonReader implements Closeable { if (skipping) { return "skipped!"; } else if (builder == null) { - return new String(buffer, start, pos - start - 1); + return stringPool.get(buffer, start, pos - start - 1); } else { builder.append(buffer, start, pos - start - 1); return builder.toString(); @@ -930,7 +937,7 @@ public final class JsonReader implements Closeable { } else if (skipping) { result = "skipped!"; } else if (builder == null) { - result = new String(buffer, pos, i); + result = stringPool.get(buffer, pos, i); } else { builder.append(buffer, pos, i); result = builder.toString(); @@ -964,7 +971,7 @@ public final class JsonReader implements Closeable { if (pos + 4 > limit && !fillBuffer(4)) { throw syntaxError("Unterminated escape sequence"); } - String hex = new String(buffer, pos, 4); + String hex = stringPool.get(buffer, pos, 4); pos += 4; return (char) Integer.parseInt(hex, 16); @@ -1036,7 +1043,7 @@ public final class JsonReader implements Closeable { value = FALSE; return JsonToken.BOOLEAN; } else { - value = new String(buffer, valuePos, valueLength); + value = stringPool.get(buffer, valuePos, valueLength); return decodeNumber(buffer, valuePos, valueLength); } } diff --git a/core/java/android/util/LruCache.java b/core/java/android/util/LruCache.java index 834dac3..5540000 100644 --- a/core/java/android/util/LruCache.java +++ b/core/java/android/util/LruCache.java @@ -304,7 +304,8 @@ public class LruCache<K, V> { } /** - * Returns the number of times {@link #get} returned a value. + * Returns the number of times {@link #get} returned a value that was + * already present in the cache. */ public synchronized final int hitCount() { return hitCount; diff --git a/core/java/android/util/NtpTrustedTime.java b/core/java/android/util/NtpTrustedTime.java new file mode 100644 index 0000000..5b19ecd --- /dev/null +++ b/core/java/android/util/NtpTrustedTime.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import android.net.SntpClient; +import android.os.SystemClock; + +/** + * {@link TrustedTime} that connects with a remote NTP server as its remote + * trusted time source. + * + * @hide + */ +public class NtpTrustedTime implements TrustedTime { + private String mNtpServer; + private long mNtpTimeout; + + private boolean mHasCache; + private long mCachedNtpTime; + private long mCachedNtpElapsedRealtime; + private long mCachedNtpCertainty; + + public NtpTrustedTime() { + } + + public void setNtpServer(String server, long timeout) { + mNtpServer = server; + mNtpTimeout = timeout; + } + + /** {@inheritDoc} */ + public boolean forceRefresh() { + if (mNtpServer == null) { + // missing server, so no trusted time available + return false; + } + + final SntpClient client = new SntpClient(); + if (client.requestTime(mNtpServer, (int) mNtpTimeout)) { + mHasCache = true; + mCachedNtpTime = client.getNtpTime(); + mCachedNtpElapsedRealtime = client.getNtpTimeReference(); + mCachedNtpCertainty = client.getRoundTripTime() / 2; + return true; + } else { + return false; + } + } + + /** {@inheritDoc} */ + public boolean hasCache() { + return mHasCache; + } + + /** {@inheritDoc} */ + public long getCacheAge() { + if (mHasCache) { + return SystemClock.elapsedRealtime() - mCachedNtpElapsedRealtime; + } else { + return Long.MAX_VALUE; + } + } + + /** {@inheritDoc} */ + public long getCacheCertainty() { + if (mHasCache) { + return mCachedNtpCertainty; + } else { + return Long.MAX_VALUE; + } + } + + /** {@inheritDoc} */ + public long currentTimeMillis() { + if (!mHasCache) { + throw new IllegalStateException("Missing authoritative time source"); + } + + // current time is age after the last ntp cache; callers who + // want fresh values will hit makeAuthoritative() first. + return mCachedNtpTime + getCacheAge(); + } +} diff --git a/core/java/android/util/TimeUtils.java b/core/java/android/util/TimeUtils.java index 9042505..93299eb 100644 --- a/core/java/android/util/TimeUtils.java +++ b/core/java/android/util/TimeUtils.java @@ -19,7 +19,7 @@ package android.util; import android.content.res.Resources; import android.content.res.XmlResourceParser; -import org.apache.harmony.luni.internal.util.ZoneInfoDB; +import libcore.util.ZoneInfoDB; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; diff --git a/core/java/android/util/TrustedTime.java b/core/java/android/util/TrustedTime.java new file mode 100644 index 0000000..263d782 --- /dev/null +++ b/core/java/android/util/TrustedTime.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +/** + * Interface that provides trusted time information, possibly coming from an NTP + * server. Implementations may cache answers until {@link #forceRefresh()}. + * + * @hide + */ +public interface TrustedTime { + /** + * Force update with an external trusted time source, returning {@code true} + * when successful. + */ + public boolean forceRefresh(); + + /** + * Check if this instance has cached a response from a trusted time source. + */ + public boolean hasCache(); + + /** + * Return time since last trusted time source contact, or + * {@link Long#MAX_VALUE} if never contacted. + */ + public long getCacheAge(); + + /** + * Return certainty of cached trusted time in milliseconds, or + * {@link Long#MAX_VALUE} if never contacted. Smaller values are more + * precise. + */ + public long getCacheCertainty(); + + /** + * Return current time similar to {@link System#currentTimeMillis()}, + * possibly using a cached authoritative time source. + */ + public long currentTimeMillis(); +} diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java index b0c33e5..041e8a8 100644 --- a/core/java/android/util/Xml.java +++ b/core/java/android/util/Xml.java @@ -16,23 +16,21 @@ package android.util; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import org.apache.harmony.xml.ExpatReader; +import org.kxml2.io.KXmlParser; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlSerializer; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.io.UnsupportedEncodingException; - -import org.apache.harmony.xml.ExpatPullParser; -import org.apache.harmony.xml.ExpatReader; +import org.xmlpull.v1.XmlSerializer; /** * XML utility methods. @@ -46,7 +44,7 @@ public class Xml { * @see <a href="http://xmlpull.org/v1/doc/features.html#relaxed"> * specification</a> */ - public static String FEATURE_RELAXED = ExpatPullParser.FEATURE_RELAXED; + public static String FEATURE_RELAXED = "http://xmlpull.org/v1/doc/features.html#relaxed"; /** * Parses the given xml string and fires events on the given SAX handler. @@ -57,8 +55,7 @@ public class Xml { XMLReader reader = new ExpatReader(); reader.setContentHandler(contentHandler); reader.parse(new InputSource(new StringReader(xml))); - } - catch (IOException e) { + } catch (IOException e) { throw new AssertionError(e); } } @@ -88,16 +85,17 @@ public class Xml { } /** - * Creates a new pull parser with namespace support. - * - * <p><b>Note:</b> This is actually slower than the SAX parser, and it's not - * fully implemented. If you need a fast, mostly implemented pull parser, - * use this. If you need a complete implementation, use KXML. + * Returns a new pull parser with namespace support. */ public static XmlPullParser newPullParser() { - ExpatPullParser parser = new ExpatPullParser(); - parser.setNamespaceProcessingEnabled(true); - return parser; + try { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + return parser; + } catch (XmlPullParserException e) { + throw new AssertionError(); + } } /** diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index 14f2e9d..2b79a76 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -39,6 +39,12 @@ import android.text.TextUtils; * An implementation of Canvas on top of OpenGL ES 2.0. */ class GLES20Canvas extends HardwareCanvas { + // Must match modifiers used in the JNI layer + private static final int MODIFIER_NONE = 0; + private static final int MODIFIER_SHADOW = 1; + private static final int MODIFIER_SHADER = 2; + private static final int MODIFIER_COLOR_FILTER = 4; + private final boolean mOpaque; private int mRenderer; @@ -154,8 +160,10 @@ class GLES20Canvas extends HardwareCanvas { // Hardware layers /////////////////////////////////////////////////////////////////////////// + static native int nCreateTextureLayer(int[] layerInfo); static native int nCreateLayer(int width, int height, boolean isOpaque, int[] layerInfo); static native void nResizeLayer(int layerId, int width, int height, int[] layerInfo); + static native void nUpdateTextureLayer(int layerId, int width, int height, int surface); static native void nDestroyLayer(int layerId); static native void nDestroyLayerDeferred(int layerId); @@ -253,20 +261,27 @@ class GLES20Canvas extends HardwareCanvas { private static native boolean nDrawDisplayList(int renderer, int displayList, int width, int height, Rect dirty); + @Override + void outputDisplayList(DisplayList displayList) { + nOutputDisplayList(mRenderer, ((GLES20DisplayList) displayList).mNativeDisplayList); + } + + private static native void nOutputDisplayList(int renderer, int displayList); + /////////////////////////////////////////////////////////////////////////// // Hardware layer /////////////////////////////////////////////////////////////////////////// void drawHardwareLayer(HardwareLayer layer, float x, float y, Paint paint) { final GLES20Layer glLayer = (GLES20Layer) layer; - boolean hasColorFilter = paint != null && setupColorFilter(paint); + int modifier = paint != null ? setupColorFilter(paint) : MODIFIER_NONE; final int nativePaint = paint == null ? 0 : paint.mNativePaint; nDrawLayer(mRenderer, glLayer.getLayer(), x, y, nativePaint); - if (hasColorFilter) nResetModifiers(mRenderer); + if (modifier != MODIFIER_NONE) nResetModifiers(mRenderer, modifier); } private static native void nDrawLayer(int renderer, int layer, float x, float y, int paint); - + void interrupt() { nInterrupt(mRenderer); } @@ -455,10 +470,10 @@ class GLES20Canvas extends HardwareCanvas { public int saveLayer(float left, float top, float right, float bottom, Paint paint, int saveFlags) { if (left < right && top < bottom) { - boolean hasColorFilter = paint != null && setupColorFilter(paint); + int modifier = paint != null ? setupColorFilter(paint) : MODIFIER_NONE; final int nativePaint = paint == null ? 0 : paint.mNativePaint; int count = nSaveLayer(mRenderer, left, top, right, bottom, nativePaint, saveFlags); - if (hasColorFilter) nResetModifiers(mRenderer); + if (modifier != MODIFIER_NONE) nResetModifiers(mRenderer, modifier); return count; } return save(saveFlags); @@ -527,10 +542,10 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint) { - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); nDrawArc(mRenderer, oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, useCenter, paint.mNativePaint); - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawArc(int renderer, float left, float top, @@ -545,11 +560,11 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawPatch(Bitmap bitmap, byte[] chunks, RectF dst, Paint paint) { // Shaders are ignored when drawing patches - boolean hasColorFilter = paint != null && setupColorFilter(paint); + int modifier = paint != null ? setupColorFilter(paint) : MODIFIER_NONE; final int nativePaint = paint == null ? 0 : paint.mNativePaint; nDrawPatch(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, chunks, dst.left, dst.top, dst.right, dst.bottom, nativePaint); - if (hasColorFilter) nResetModifiers(mRenderer); + if (modifier != MODIFIER_NONE) nResetModifiers(mRenderer, modifier); } private static native void nDrawPatch(int renderer, int bitmap, byte[] buffer, byte[] chunks, @@ -558,10 +573,10 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) { // Shaders are ignored when drawing bitmaps - boolean hasColorFilter = paint != null && setupColorFilter(paint); + int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; final int nativePaint = paint == null ? 0 : paint.mNativePaint; nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, left, top, nativePaint); - if (hasColorFilter) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawBitmap( @@ -570,11 +585,11 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) { // Shaders are ignored when drawing bitmaps - boolean hasColorFilter = paint != null && setupColorFilter(paint); + int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; final int nativePaint = paint == null ? 0 : paint.mNativePaint; nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, matrix.native_instance, nativePaint); - if (hasColorFilter) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawBitmap(int renderer, int bitmap, byte[] buff, @@ -583,7 +598,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) { // Shaders are ignored when drawing bitmaps - boolean hasColorFilter = paint != null && setupColorFilter(paint); + int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; final int nativePaint = paint == null ? 0 : paint.mNativePaint; int left, top, right, bottom; @@ -600,17 +615,17 @@ class GLES20Canvas extends HardwareCanvas { nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, left, top, right, bottom, dst.left, dst.top, dst.right, dst.bottom, nativePaint); - if (hasColorFilter) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } @Override public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) { // Shaders are ignored when drawing bitmaps - boolean hasColorFilter = paint != null && setupColorFilter(paint); + int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; final int nativePaint = paint == null ? 0 : paint.mNativePaint; nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, src.left, src.top, src.right, src.bottom, dst.left, dst.top, dst.right, dst.bottom, nativePaint); - if (hasColorFilter) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawBitmap(int renderer, int bitmap, byte[] buffer, @@ -621,13 +636,13 @@ class GLES20Canvas extends HardwareCanvas { public void drawBitmap(int[] colors, int offset, int stride, float x, float y, int width, int height, boolean hasAlpha, Paint paint) { // Shaders are ignored when drawing bitmaps - boolean hasColorFilter = paint != null && setupColorFilter(paint); + int modifier = paint != null ? setupColorFilter(paint) : MODIFIER_NONE; final Bitmap.Config config = hasAlpha ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565; final Bitmap b = Bitmap.createBitmap(colors, offset, stride, width, height, config); final int nativePaint = paint == null ? 0 : paint.mNativePaint; nDrawBitmap(mRenderer, b.mNativeBitmap, b.mBuffer, x, y, nativePaint); b.recycle(); - if (hasColorFilter) nResetModifiers(mRenderer); + if (modifier != MODIFIER_NONE) nResetModifiers(mRenderer, modifier); } @Override @@ -655,11 +670,11 @@ class GLES20Canvas extends HardwareCanvas { colors = null; colorOffset = 0; - boolean hasColorFilter = paint != null && setupColorFilter(paint); + int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; final int nativePaint = paint == null ? 0 : paint.mNativePaint; nDrawBitmapMesh(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset, nativePaint); - if (hasColorFilter) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawBitmapMesh(int renderer, int bitmap, byte[] buffer, @@ -668,9 +683,9 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawCircle(float cx, float cy, float radius, Paint paint) { - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); nDrawCircle(mRenderer, cx, cy, radius, paint.mNativePaint); - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawCircle(int renderer, float cx, float cy, @@ -702,9 +717,9 @@ class GLES20Canvas extends HardwareCanvas { if ((offset | count) < 0 || offset + count > pts.length) { throw new IllegalArgumentException("The lines array must contain 4 elements per line."); } - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); nDrawLines(mRenderer, pts, offset, count, paint.mNativePaint); - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawLines(int renderer, float[] points, @@ -717,9 +732,9 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawOval(RectF oval, Paint paint) { - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); nDrawOval(mRenderer, oval.left, oval.top, oval.right, oval.bottom, paint.mNativePaint); - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawOval(int renderer, float left, float top, @@ -734,7 +749,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawPath(Path path, Paint paint) { - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); if (path.isSimplePath) { if (path.rects != null) { nDrawRects(mRenderer, path.rects.mNativeRegion, paint.mNativePaint); @@ -742,7 +757,7 @@ class GLES20Canvas extends HardwareCanvas { } else { nDrawPath(mRenderer, path.mNativePath, paint.mNativePaint); } - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawPath(int renderer, int path, int paint); @@ -767,19 +782,24 @@ class GLES20Canvas extends HardwareCanvas { public void drawPoint(float x, float y, Paint paint) { mPoint[0] = x; mPoint[1] = y; - drawPoints(mPoint, 0, 1, paint); + drawPoints(mPoint, 0, 2, paint); } @Override - public void drawPoints(float[] pts, int offset, int count, Paint paint) { - // TODO: Implement + public void drawPoints(float[] pts, Paint paint) { + drawPoints(pts, 0, pts.length, paint); } @Override - public void drawPoints(float[] pts, Paint paint) { - drawPoints(pts, 0, pts.length / 2, paint); + public void drawPoints(float[] pts, int offset, int count, Paint paint) { + int modifiers = setupModifiers(paint); + nDrawPoints(mRenderer, pts, offset, count, paint.mNativePaint); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } + private static native void nDrawPoints(int renderer, float[] points, + int offset, int count, int paint); + @Override public void drawPosText(char[] text, int index, int count, float[] pos, Paint paint) { // TODO: Implement @@ -792,9 +812,9 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawRect(float left, float top, float right, float bottom, Paint paint) { - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); nDrawRect(mRenderer, left, top, right, bottom, paint.mNativePaint); - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawRect(int renderer, float left, float top, @@ -817,10 +837,10 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawRoundRect(RectF rect, float rx, float ry, Paint paint) { - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); nDrawRoundRect(mRenderer, rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint.mNativePaint); - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } private static native void nDrawRoundRect(int renderer, float left, float top, @@ -832,11 +852,11 @@ class GLES20Canvas extends HardwareCanvas { throw new IndexOutOfBoundsException(); } - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); try { nDrawText(mRenderer, text, index, count, x, y, paint.mBidiFlags, paint.mNativePaint); } finally { - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } @@ -845,7 +865,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) { - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); try { if (text instanceof String || text instanceof SpannedString || text instanceof SpannableString) { @@ -862,7 +882,7 @@ class GLES20Canvas extends HardwareCanvas { TemporaryBuffer.recycle(buf); } } finally { - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } @@ -872,11 +892,11 @@ class GLES20Canvas extends HardwareCanvas { throw new IndexOutOfBoundsException(); } - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); try { nDrawText(mRenderer, text, start, end, x, y, paint.mBidiFlags, paint.mNativePaint); } finally { - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } @@ -885,12 +905,12 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawText(String text, float x, float y, Paint paint) { - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); try { nDrawText(mRenderer, text, 0, text.length(), x, y, paint.mBidiFlags, paint.mNativePaint); } finally { - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } @@ -915,12 +935,12 @@ class GLES20Canvas extends HardwareCanvas { throw new IllegalArgumentException("Unknown direction: " + dir); } - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); try { nDrawTextRun(mRenderer, text, index, count, contextIndex, contextCount, x, y, dir, paint.mNativePaint); } finally { - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } @@ -934,7 +954,7 @@ class GLES20Canvas extends HardwareCanvas { throw new IndexOutOfBoundsException(); } - boolean hasModifier = setupModifiers(paint); + int modifiers = setupModifiers(paint); try { int flags = dir == 0 ? 0 : 1; if (text instanceof String || text instanceof SpannedString || @@ -954,7 +974,7 @@ class GLES20Canvas extends HardwareCanvas { TemporaryBuffer.recycle(buf); } } finally { - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } @@ -968,43 +988,57 @@ class GLES20Canvas extends HardwareCanvas { // TODO: Implement } - private boolean setupModifiers(Paint paint) { - boolean hasModifier = false; + private int setupModifiers(Bitmap b, Paint paint) { + if (b.getConfig() == Bitmap.Config.ALPHA_8) { + return setupModifiers(paint); + } + + final ColorFilter filter = paint.getColorFilter(); + if (filter != null) { + nSetupColorFilter(mRenderer, filter.nativeColorFilter); + return MODIFIER_COLOR_FILTER; + } + + return MODIFIER_NONE; + } + + private int setupModifiers(Paint paint) { + int modifiers = MODIFIER_NONE; if (paint.hasShadow) { nSetupShadow(mRenderer, paint.shadowRadius, paint.shadowDx, paint.shadowDy, paint.shadowColor); - hasModifier = true; + modifiers |= MODIFIER_SHADOW; } final Shader shader = paint.getShader(); if (shader != null) { nSetupShader(mRenderer, shader.native_shader); - hasModifier = true; + modifiers |= MODIFIER_SHADER; } final ColorFilter filter = paint.getColorFilter(); if (filter != null) { nSetupColorFilter(mRenderer, filter.nativeColorFilter); - hasModifier = true; + modifiers |= MODIFIER_COLOR_FILTER; } - return hasModifier; + return modifiers; } - private boolean setupColorFilter(Paint paint) { + private int setupColorFilter(Paint paint) { final ColorFilter filter = paint.getColorFilter(); if (filter != null) { nSetupColorFilter(mRenderer, filter.nativeColorFilter); - return true; + return MODIFIER_COLOR_FILTER; } - return false; + return MODIFIER_NONE; } - + private static native void nSetupShader(int renderer, int shader); private static native void nSetupColorFilter(int renderer, int colorFilter); private static native void nSetupShadow(int renderer, float radius, float dx, float dy, int color); - private static native void nResetModifiers(int renderer); + private static native void nResetModifiers(int renderer, int modifiers); } diff --git a/core/java/android/view/GLES20Layer.java b/core/java/android/view/GLES20Layer.java index 6000a4a..bc191a6 100644 --- a/core/java/android/view/GLES20Layer.java +++ b/core/java/android/view/GLES20Layer.java @@ -14,39 +14,21 @@ * limitations under the License. */ -package android.view; -import android.graphics.Canvas; +package android.view; /** * An OpenGL ES 2.0 implementation of {@link HardwareLayer}. */ -class GLES20Layer extends HardwareLayer { - private int mLayer; - - private int mLayerWidth; - private int mLayerHeight; - - private final GLES20Canvas mCanvas; +abstract class GLES20Layer extends HardwareLayer { + int mLayer; + Finalizer mFinalizer; - @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) - private final Finalizer mFinalizer; - - GLES20Layer(int width, int height, boolean isOpaque) { - super(width, height, isOpaque); - - int[] layerInfo = new int[2]; - mLayer = GLES20Canvas.nCreateLayer(width, height, isOpaque, layerInfo); - if (mLayer != 0) { - mLayerWidth = layerInfo[0]; - mLayerHeight = layerInfo[1]; + GLES20Layer() { + } - mCanvas = new GLES20Canvas(mLayer, !isOpaque); - mFinalizer = new Finalizer(mLayer); - } else { - mCanvas = null; - mFinalizer = null; - } + GLES20Layer(int width, int height, boolean opaque) { + super(width, height, opaque); } /** @@ -58,55 +40,14 @@ class GLES20Layer extends HardwareLayer { return mLayer; } - @Override - boolean isValid() { - return mLayer != 0 && mLayerWidth > 0 && mLayerHeight > 0; - } - - @Override - void resize(int width, int height) { - if (!isValid() || width <= 0 || height <= 0) return; - - mWidth = width; - mHeight = height; - - if (width != mLayerWidth || height != mLayerHeight) { - int[] layerInfo = new int[2]; - - GLES20Canvas.nResizeLayer(mLayer, width, height, layerInfo); - - mLayerWidth = layerInfo[0]; - mLayerHeight = layerInfo[1]; - } - } - - @Override - HardwareCanvas getCanvas() { - return mCanvas; - } - - @Override - void end(Canvas currentCanvas) { - if (currentCanvas instanceof GLES20Canvas) { - ((GLES20Canvas) currentCanvas).resume(); - } - } - - @Override - HardwareCanvas start(Canvas currentCanvas) { - if (currentCanvas instanceof GLES20Canvas) { - ((GLES20Canvas) currentCanvas).interrupt(); - } - return getCanvas(); - } - + @Override void destroy() { - mFinalizer.destroy(); + if (mFinalizer != null) mFinalizer.destroy(); mLayer = 0; } - private static class Finalizer { + static class Finalizer { private int mLayerId; public Finalizer(int layerId) { diff --git a/core/java/android/view/GLES20RenderLayer.java b/core/java/android/view/GLES20RenderLayer.java new file mode 100644 index 0000000..7adac1c --- /dev/null +++ b/core/java/android/view/GLES20RenderLayer.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Canvas; + +/** + * An OpenGL ES 2.0 implementation of {@link HardwareLayer}. This + * implementation can be used a rendering target. It generates a + * {@link Canvas} that can be used to render into an FBO using OpenGL. + */ +class GLES20RenderLayer extends GLES20Layer { + + private int mLayerWidth; + private int mLayerHeight; + + private final GLES20Canvas mCanvas; + + GLES20RenderLayer(int width, int height, boolean isOpaque) { + super(width, height, isOpaque); + + int[] layerInfo = new int[2]; + mLayer = GLES20Canvas.nCreateLayer(width, height, isOpaque, layerInfo); + if (mLayer != 0) { + mLayerWidth = layerInfo[0]; + mLayerHeight = layerInfo[1]; + + mCanvas = new GLES20Canvas(mLayer, !isOpaque); + mFinalizer = new Finalizer(mLayer); + } else { + mCanvas = null; + mFinalizer = null; + } + } + + @Override + boolean isValid() { + return mLayer != 0 && mLayerWidth > 0 && mLayerHeight > 0; + } + + @Override + void resize(int width, int height) { + if (!isValid() || width <= 0 || height <= 0) return; + + mWidth = width; + mHeight = height; + + if (width != mLayerWidth || height != mLayerHeight) { + int[] layerInfo = new int[2]; + + GLES20Canvas.nResizeLayer(mLayer, width, height, layerInfo); + + mLayerWidth = layerInfo[0]; + mLayerHeight = layerInfo[1]; + } + } + + @Override + HardwareCanvas getCanvas() { + return mCanvas; + } + + @Override + void end(Canvas currentCanvas) { + if (currentCanvas instanceof GLES20Canvas) { + ((GLES20Canvas) currentCanvas).resume(); + } + } + + @Override + HardwareCanvas start(Canvas currentCanvas) { + if (currentCanvas instanceof GLES20Canvas) { + ((GLES20Canvas) currentCanvas).interrupt(); + } + return getCanvas(); + } +} diff --git a/core/java/android/view/GLES20TextureLayer.java b/core/java/android/view/GLES20TextureLayer.java new file mode 100644 index 0000000..fcf421b --- /dev/null +++ b/core/java/android/view/GLES20TextureLayer.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Canvas; +import android.graphics.SurfaceTexture; + +/** + * An OpenGL ES 2.0 implementation of {@link HardwareLayer}. This + * implementation can be used as a texture. Rendering into this + * layer is not controlled by a {@link HardwareCanvas}. + */ +class GLES20TextureLayer extends GLES20Layer { + private int mTexture; + private SurfaceTexture mSurface; + + GLES20TextureLayer() { + int[] layerInfo = new int[2]; + mLayer = GLES20Canvas.nCreateTextureLayer(layerInfo); + + if (mLayer != 0) { + mTexture = layerInfo[0]; + mFinalizer = new Finalizer(mLayer); + } else { + mFinalizer = null; + } + } + + @Override + boolean isValid() { + return mLayer != 0 && mTexture != 0; + } + + @Override + void resize(int width, int height) { + } + + @Override + HardwareCanvas getCanvas() { + return null; + } + + @Override + HardwareCanvas start(Canvas currentCanvas) { + return null; + } + + @Override + void end(Canvas currentCanvas) { + } + + SurfaceTexture getSurfaceTexture() { + if (mSurface == null) { + mSurface = new SurfaceTexture(mTexture); + } + return mSurface; + } + + void update(int width, int height, int surface) { + GLES20Canvas.nUpdateTextureLayer(mLayer, width, height, surface); + } +} diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index 79b3d42..1ccc66f 100755..100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -193,8 +193,10 @@ public class GestureDetector { } } + // TODO: ViewConfiguration + private int mBiggerTouchSlopSquare = 20 * 20; + private int mTouchSlopSquare; - private int mLargeTouchSlopSquare; private int mDoubleTapSlopSquare; private int mMinimumFlingVelocity; private int mMaximumFlingVelocity; @@ -243,6 +245,13 @@ public class GestureDetector { */ private VelocityTracker mVelocityTracker; + /** + * Consistency verifier for debugging purposes. + */ + private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = + InputEventConsistencyVerifier.isInstrumentationEnabled() ? + new InputEventConsistencyVerifier(this, 0) : null; + private class GestureHandler extends Handler { GestureHandler() { super(); @@ -382,11 +391,10 @@ public class GestureDetector { mIgnoreMultitouch = ignoreMultitouch; // Fallback to support pre-donuts releases - int touchSlop, largeTouchSlop, doubleTapSlop; + int touchSlop, doubleTapSlop; if (context == null) { //noinspection deprecation touchSlop = ViewConfiguration.getTouchSlop(); - largeTouchSlop = touchSlop + 2; doubleTapSlop = ViewConfiguration.getDoubleTapSlop(); //noinspection deprecation mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); @@ -394,13 +402,11 @@ public class GestureDetector { } else { final ViewConfiguration configuration = ViewConfiguration.get(context); touchSlop = configuration.getScaledTouchSlop(); - largeTouchSlop = configuration.getScaledLargeTouchSlop(); doubleTapSlop = configuration.getScaledDoubleTapSlop(); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity(); } mTouchSlopSquare = touchSlop * touchSlop; - mLargeTouchSlopSquare = largeTouchSlop * largeTouchSlop; mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; } @@ -444,6 +450,10 @@ public class GestureDetector { * else false. */ public boolean onTouchEvent(MotionEvent ev) { + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTouchEvent(ev, 0); + } + final int action = ev.getAction(); final float y = ev.getY(); final float x = ev.getX(); @@ -535,7 +545,7 @@ public class GestureDetector { mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); } - if (distance > mLargeTouchSlopSquare) { + if (distance > mBiggerTouchSlopSquare) { mAlwaysInBiggerTapRegion = false; } } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { @@ -580,8 +590,14 @@ public class GestureDetector { mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); break; + case MotionEvent.ACTION_CANCEL: cancel(); + break; + } + + if (!handled && mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0); } return handled; } diff --git a/core/java/android/view/HardwareCanvas.java b/core/java/android/view/HardwareCanvas.java index caa7b74..23b3abc 100644 --- a/core/java/android/view/HardwareCanvas.java +++ b/core/java/android/view/HardwareCanvas.java @@ -64,6 +64,14 @@ public abstract class HardwareCanvas extends Canvas { abstract boolean drawDisplayList(DisplayList displayList, int width, int height, Rect dirty); /** + * Outputs the specified display list to the log. This method exists for use by + * tools to output display lists for selected nodes to the log. + * + * @param displayList The display list to be logged. + */ + abstract void outputDisplayList(DisplayList displayList); + + /** * Draws the specified layer onto this canvas. * * @param layer The layer to composite on this canvas diff --git a/core/java/android/view/HardwareLayer.java b/core/java/android/view/HardwareLayer.java index d01b8ce..86dec3f 100644 --- a/core/java/android/view/HardwareLayer.java +++ b/core/java/android/view/HardwareLayer.java @@ -26,12 +26,24 @@ import android.graphics.Canvas; * drawn several times. */ abstract class HardwareLayer { + /** + * Indicates an unknown dimension (width or height.) + */ + static final int DIMENSION_UNDEFINED = -1; + int mWidth; int mHeight; final boolean mOpaque; /** + * Creates a new hardware layer with undefined dimensions. + */ + HardwareLayer() { + this(DIMENSION_UNDEFINED, DIMENSION_UNDEFINED, false); + } + + /** * Creates a new hardware layer at least as large as the supplied * dimensions. * diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index 8584bf2..845fbc3 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -20,7 +20,8 @@ package android.view; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; -import android.os.SystemClock; +import android.graphics.SurfaceTexture; +import android.os.*; import android.util.EventLog; import android.util.Log; @@ -33,7 +34,7 @@ import javax.microedition.khronos.egl.EGLSurface; import javax.microedition.khronos.opengles.GL; /** - * Interface for rendering a ViewRoot using hardware acceleration. + * Interface for rendering a ViewAncestor using hardware acceleration. * * @hide */ @@ -166,6 +167,14 @@ public abstract class HardwareRenderer { abstract DisplayList createDisplayList(View v); /** + * Creates a new hardware layer. A hardware layer built by calling this + * method will be treated as a texture layer, instead of as a render target. + * + * @return A hardware layer + */ + abstract HardwareLayer createHardwareLayer(); + + /** * Creates a new hardware layer. * * @param width The minimum width of the layer @@ -175,10 +184,32 @@ public abstract class HardwareRenderer { * @return A hardware layer */ abstract HardwareLayer createHardwareLayer(int width, int height, boolean isOpaque); + + /** + * Creates a new {@link SurfaceTexture} that can be used to render into the + * specified hardware layer. + * + * + * @param layer The layer to render into using a {@link android.graphics.SurfaceTexture} + * + * @return A {@link SurfaceTexture} + */ + abstract SurfaceTexture createSuraceTexture(HardwareLayer layer); + + /** + * Updates the specified layer. + * + * @param layer The hardware layer to update + * @param width The layer's width + * @param height The layer's height + * @param surface The surface to update + */ + abstract void updateTextureLayer(HardwareLayer layer, int width, int height, + SurfaceTexture surface); /** * Initializes the hardware renderer for the specified surface and setup the - * renderer for drawing, if needed. This is invoked when the ViewRoot has + * renderer for drawing, if needed. This is invoked when the ViewAncestor has * potentially lost the hardware renderer. The hardware renderer should be * reinitialized and setup when the render {@link #isRequested()} and * {@link #isEnabled()}. @@ -256,9 +287,11 @@ public abstract class HardwareRenderer { @SuppressWarnings({"deprecation"}) static abstract class GlRenderer extends HardwareRenderer { - private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; - private static final int EGL_SURFACE_TYPE = 0x3033; - private static final int EGL_SWAP_BEHAVIOR_PRESERVED_BIT = 0x0400; + // These values are not exposed in our EGL APIs + static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + static final int EGL_SURFACE_TYPE = 0x3033; + static final int EGL_SWAP_BEHAVIOR_PRESERVED_BIT = 0x0400; + static final int EGL_OPENGL_ES2_BIT = 4; private static final int SURFACE_STATE_ERROR = 0; private static final int SURFACE_STATE_SUCCESS = 1; @@ -290,7 +323,7 @@ public abstract class HardwareRenderer { GlRenderer(int glVersion, boolean translucent) { mGlVersion = glVersion; mTranslucent = translucent; - final String dirtyProperty = System.getProperty(RENDER_DIRTY_REGIONS_PROPERTY, "true"); + final String dirtyProperty = SystemProperties.get(RENDER_DIRTY_REGIONS_PROPERTY, "true"); //noinspection PointlessBooleanExpression,ConstantConditions mDirtyRegions = RENDER_DIRTY_REGIONS && "true".equalsIgnoreCase(dirtyProperty); } @@ -426,13 +459,12 @@ public abstract class HardwareRenderer { getEGLErrorString(sEgl.eglGetError())); } - sEglConfig = getConfigChooser(mGlVersion).chooseConfig(sEgl, sEglDisplay); + sEglConfig = chooseEglConfig(); if (sEglConfig == null) { // We tried to use EGL_SWAP_BEHAVIOR_PRESERVED_BIT, try again without if (mDirtyRegions) { mDirtyRegions = false; - - sEglConfig = getConfigChooser(mGlVersion).chooseConfig(sEgl, sEglDisplay); + sEglConfig = chooseEglConfig(); if (sEglConfig == null) { throw new RuntimeException("eglConfig not initialized"); } @@ -448,6 +480,21 @@ public abstract class HardwareRenderer { sEglContext = createContext(sEgl, sEglDisplay, sEglConfig); } + private EGLConfig chooseEglConfig() { + int[] configsCount = new int[1]; + EGLConfig[] configs = new EGLConfig[1]; + int[] configSpec = getConfig(mDirtyRegions); + if (!sEgl.eglChooseConfig(sEglDisplay, configSpec, configs, 1, configsCount)) { + throw new IllegalArgumentException("eglChooseConfig failed " + + getEGLErrorString(sEgl.eglGetError())); + } else if (configsCount[0] > 0) { + return configs[0]; + } + return null; + } + + abstract int[] getConfig(boolean dirtyRegions); + GL createEglSurface(SurfaceHolder holder) throws Surface.OutOfResourcesException { // Check preconditions. if (sEgl == null) { @@ -559,15 +606,6 @@ public abstract class HardwareRenderer { void onPostDraw() { } - - /** - * Defines the EGL configuration for this renderer. - * - * @return An {@link android.view.HardwareRenderer.GlRenderer.EglConfigChooser}. - */ - EglConfigChooser getConfigChooser(int glVersion) { - return new ComponentSizeChooser(glVersion, 8, 8, 8, 8, 0, 0, mDirtyRegions); - } @Override void draw(View view, View.AttachInfo attachInfo, HardwareDrawCallbacks callbacks, @@ -644,8 +682,21 @@ public abstract class HardwareRenderer { } attachInfo.mIgnoreDirtyState = false; - + + final long swapBuffersStartTime; + if (ViewDebug.DEBUG_LATENCY) { + swapBuffersStartTime = System.nanoTime(); + } + sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(LOG_TAG, "Latency: Spent " + + ((now - swapBuffersStartTime) * 0.000001f) + + "ms waiting for eglSwapBuffers()"); + } + checkEglErrors(); } } @@ -667,134 +718,6 @@ public abstract class HardwareRenderer { } return SURFACE_STATE_SUCCESS; } - - static abstract class EglConfigChooser { - final int[] mConfigSpec; - private final int mGlVersion; - - EglConfigChooser(int glVersion, int[] configSpec) { - mGlVersion = glVersion; - mConfigSpec = filterConfigSpec(configSpec); - } - - EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { - int[] index = new int[1]; - if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, index)) { - throw new IllegalArgumentException("eglChooseConfig failed " - + getEGLErrorString(egl.eglGetError())); - } - - int numConfigs = index[0]; - if (numConfigs <= 0) { - throw new IllegalArgumentException("No configs match configSpec"); - } - - EGLConfig[] configs = new EGLConfig[numConfigs]; - if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, index)) { - throw new IllegalArgumentException("eglChooseConfig failed " - + getEGLErrorString(egl.eglGetError())); - } - - EGLConfig config = chooseConfig(egl, display, configs); - if (config == null) { - throw new IllegalArgumentException("No config chosen"); - } - - return config; - } - - abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs); - - private int[] filterConfigSpec(int[] configSpec) { - if (mGlVersion != 2) { - return configSpec; - } - /* We know none of the subclasses define EGL_RENDERABLE_TYPE. - * And we know the configSpec is well formed. - */ - int len = configSpec.length; - int[] newConfigSpec = new int[len + 2]; - System.arraycopy(configSpec, 0, newConfigSpec, 0, len - 1); - newConfigSpec[len - 1] = EGL10.EGL_RENDERABLE_TYPE; - newConfigSpec[len] = 4; /* EGL_OPENGL_ES2_BIT */ - newConfigSpec[len + 1] = EGL10.EGL_NONE; - return newConfigSpec; - } - } - - /** - * Choose a configuration with exactly the specified r,g,b,a sizes, - * and at least the specified depth and stencil sizes. - */ - static class ComponentSizeChooser extends EglConfigChooser { - private int[] mValue; - - private final int mRedSize; - private final int mGreenSize; - private final int mBlueSize; - private final int mAlphaSize; - private final int mDepthSize; - private final int mStencilSize; - private final boolean mDirtyRegions; - - ComponentSizeChooser(int glVersion, int redSize, int greenSize, int blueSize, - int alphaSize, int depthSize, int stencilSize, boolean dirtyRegions) { - super(glVersion, new int[] { - EGL10.EGL_RED_SIZE, redSize, - EGL10.EGL_GREEN_SIZE, greenSize, - EGL10.EGL_BLUE_SIZE, blueSize, - EGL10.EGL_ALPHA_SIZE, alphaSize, - EGL10.EGL_DEPTH_SIZE, depthSize, - EGL10.EGL_STENCIL_SIZE, stencilSize, - EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT | - (dirtyRegions ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0), - EGL10.EGL_NONE }); - mValue = new int[1]; - mRedSize = redSize; - mGreenSize = greenSize; - mBlueSize = blueSize; - mAlphaSize = alphaSize; - mDepthSize = depthSize; - mStencilSize = stencilSize; - mDirtyRegions = dirtyRegions; - } - - @Override - EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { - for (EGLConfig config : configs) { - int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); - int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); - if (d >= mDepthSize && s >= mStencilSize) { - int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); - int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); - int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); - int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); - boolean backBuffer; - if (mDirtyRegions) { - int surfaceType = findConfigAttrib(egl, display, config, - EGL_SURFACE_TYPE, 0); - backBuffer = (surfaceType & EGL_SWAP_BEHAVIOR_PRESERVED_BIT) != 0; - } else { - backBuffer = true; - } - if (r >= mRedSize && g >= mGreenSize && b >= mBlueSize && a >= mAlphaSize - && backBuffer) { - return config; - } - } - } - return null; - } - - private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, - int attribute, int defaultValue) { - if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { - return mValue[0]; - } - - return defaultValue; - } - } } /** @@ -811,7 +734,23 @@ public abstract class HardwareRenderer { GLES20Canvas createCanvas() { return mGlCanvas = new GLES20Canvas(mTranslucent); } - + + @Override + int[] getConfig(boolean dirtyRegions) { + return new int[] { + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_RED_SIZE, 8, + EGL10.EGL_GREEN_SIZE, 8, + EGL10.EGL_BLUE_SIZE, 8, + EGL10.EGL_ALPHA_SIZE, 8, + EGL10.EGL_DEPTH_SIZE, 0, + EGL10.EGL_STENCIL_SIZE, 0, + EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT | + (dirtyRegions ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0), + EGL10.EGL_NONE + }; + } + @Override boolean canDraw() { return super.canDraw() && mGlCanvas != null; @@ -842,10 +781,26 @@ public abstract class HardwareRenderer { DisplayList createDisplayList(View v) { return new GLES20DisplayList(v); } - + + @Override + HardwareLayer createHardwareLayer() { + return new GLES20TextureLayer(); + } + @Override HardwareLayer createHardwareLayer(int width, int height, boolean isOpaque) { - return new GLES20Layer(width, height, isOpaque); + return new GLES20RenderLayer(width, height, isOpaque); + } + + @Override + SurfaceTexture createSuraceTexture(HardwareLayer layer) { + return ((GLES20TextureLayer) layer).getSurfaceTexture(); + } + + @Override + void updateTextureLayer(HardwareLayer layer, int width, int height, + SurfaceTexture surface) { + ((GLES20TextureLayer) layer).update(width, height, surface.mSurfaceTexture); } static HardwareRenderer create(boolean translucent) { diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 8cb68f9..bfc7c31 100755 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -153,7 +153,14 @@ public final class InputDevice implements Parcelable { * @see #SOURCE_CLASS_POINTER */ public static final int SOURCE_MOUSE = 0x00002000 | SOURCE_CLASS_POINTER; - + + /** + * The input source is a stylus pointing device. + * + * @see #SOURCE_CLASS_POINTER + */ + public static final int SOURCE_STYLUS = 0x00004000 | SOURCE_CLASS_POINTER; + /** * The input source is a trackball. * @@ -585,6 +592,7 @@ public final class InputDevice implements Parcelable { appendSourceDescriptionIfApplicable(description, SOURCE_DPAD, "dpad"); appendSourceDescriptionIfApplicable(description, SOURCE_TOUCHSCREEN, "touchscreen"); appendSourceDescriptionIfApplicable(description, SOURCE_MOUSE, "mouse"); + appendSourceDescriptionIfApplicable(description, SOURCE_STYLUS, "stylus"); appendSourceDescriptionIfApplicable(description, SOURCE_TRACKBALL, "trackball"); appendSourceDescriptionIfApplicable(description, SOURCE_TOUCHPAD, "touchpad"); appendSourceDescriptionIfApplicable(description, SOURCE_JOYSTICK, "joystick"); diff --git a/core/java/android/view/InputEvent.java b/core/java/android/view/InputEvent.java index f6aeb39..01ddcc9 100755 --- a/core/java/android/view/InputEvent.java +++ b/core/java/android/view/InputEvent.java @@ -67,6 +67,52 @@ public abstract class InputEvent implements Parcelable { */ public abstract void setSource(int source); + /** + * Copies the event. + * + * @return A deep copy of the event. + * @hide + */ + public abstract InputEvent copy(); + + /** + * Recycles the event. + * This method should only be used by the system since applications do not + * expect {@link KeyEvent} objects to be recycled, although {@link MotionEvent} + * objects are fine. See {@link KeyEvent#recycle()} for details. + * @hide + */ + public abstract void recycle(); + + /** + * Gets a private flag that indicates when the system has detected that this input event + * may be inconsistent with respect to the sequence of previously delivered input events, + * such as when a key up event is sent but the key was not down or when a pointer + * move event is sent but the pointer is not down. + * + * @return True if this event is tainted. + * @hide + */ + public abstract boolean isTainted(); + + /** + * Sets a private flag that indicates when the system has detected that this input event + * may be inconsistent with respect to the sequence of previously delivered input events, + * such as when a key up event is sent but the key was not down or when a pointer + * move event is sent but the pointer is not down. + * + * @param tainted True if this event is tainted. + * @hide + */ + public abstract void setTainted(boolean tainted); + + /** + * Returns the time (in ns) when this specific event was generated. + * The value is in nanosecond precision but it may not have nanosecond accuracy. + * @hide + */ + public abstract long getEventTimeNano(); + public int describeContents() { return 0; } diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java new file mode 100644 index 0000000..e14b975 --- /dev/null +++ b/core/java/android/view/InputEventConsistencyVerifier.java @@ -0,0 +1,730 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.os.Build; +import android.util.Log; + +/** + * Checks whether a sequence of input events is self-consistent. + * Logs a description of each problem detected. + * <p> + * When a problem is detected, the event is tainted. This mechanism prevents the same + * error from being reported multiple times. + * </p> + * + * @hide + */ +public final class InputEventConsistencyVerifier { + private static final boolean IS_ENG_BUILD = "eng".equals(Build.TYPE); + + // The number of recent events to log when a problem is detected. + // Can be set to 0 to disable logging recent events but the runtime overhead of + // this feature is negligible on current hardware. + private static final int RECENT_EVENTS_TO_LOG = 5; + + // The object to which the verifier is attached. + private final Object mCaller; + + // Consistency verifier flags. + private final int mFlags; + + // Tag for logging which a client can set to help distinguish the output + // from different verifiers since several can be active at the same time. + // If not provided defaults to the simple class name. + private final String mLogTag; + + // The most recently checked event and the nesting level at which it was checked. + // This is only set when the verifier is called from a nesting level greater than 0 + // so that the verifier can detect when it has been asked to verify the same event twice. + // It does not make sense to examine the contents of the last event since it may have + // been recycled. + private InputEvent mLastEvent; + private int mLastNestingLevel; + + // Copy of the most recent events. + private InputEvent[] mRecentEvents; + private boolean[] mRecentEventsUnhandled; + private int mMostRecentEventIndex; + + // Current event and its type. + private InputEvent mCurrentEvent; + private String mCurrentEventType; + + // Linked list of key state objects. + private KeyState mKeyStateList; + + // Current state of the trackball. + private boolean mTrackballDown; + private boolean mTrackballUnhandled; + + // Bitfield of pointer ids that are currently down. + // Assumes that the largest possible pointer id is 31, which is potentially subject to change. + // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) + private int mTouchEventStreamPointers; + + // The device id and source of the current stream of touch events. + private int mTouchEventStreamDeviceId = -1; + private int mTouchEventStreamSource; + + // Set to true when we discover that the touch event stream is inconsistent. + // Reset on down or cancel. + private boolean mTouchEventStreamIsTainted; + + // Set to true if the touch event stream is partially unhandled. + private boolean mTouchEventStreamUnhandled; + + // Set to true if we received hover enter. + private boolean mHoverEntered; + + // The current violation message. + private StringBuilder mViolationMessage; + + /** + * Indicates that the verifier is intended to act on raw device input event streams. + * Disables certain checks for invariants that are established by the input dispatcher + * itself as it delivers input events, such as key repeating behavior. + */ + public static final int FLAG_RAW_DEVICE_INPUT = 1 << 0; + + /** + * Creates an input consistency verifier. + * @param caller The object to which the verifier is attached. + * @param flags Flags to the verifier, or 0 if none. + */ + public InputEventConsistencyVerifier(Object caller, int flags) { + this(caller, flags, InputEventConsistencyVerifier.class.getSimpleName()); + } + + /** + * Creates an input consistency verifier. + * @param caller The object to which the verifier is attached. + * @param flags Flags to the verifier, or 0 if none. + * @param logTag Tag for logging. If null defaults to the short class name. + */ + public InputEventConsistencyVerifier(Object caller, int flags, String logTag) { + this.mCaller = caller; + this.mFlags = flags; + this.mLogTag = (logTag != null) ? logTag : "InputEventConsistencyVerifier"; + } + + /** + * Determines whether the instrumentation should be enabled. + * @return True if it should be enabled. + */ + public static boolean isInstrumentationEnabled() { + return IS_ENG_BUILD; + } + + /** + * Resets the state of the input event consistency verifier. + */ + public void reset() { + mLastEvent = null; + mLastNestingLevel = 0; + mTrackballDown = false; + mTrackballUnhandled = false; + mTouchEventStreamPointers = 0; + mTouchEventStreamIsTainted = false; + mTouchEventStreamUnhandled = false; + mHoverEntered = false; + + while (mKeyStateList != null) { + final KeyState state = mKeyStateList; + mKeyStateList = state.next; + state.recycle(); + } + } + + /** + * Checks an arbitrary input event. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onInputEvent(InputEvent event, int nestingLevel) { + if (event instanceof KeyEvent) { + final KeyEvent keyEvent = (KeyEvent)event; + onKeyEvent(keyEvent, nestingLevel); + } else { + final MotionEvent motionEvent = (MotionEvent)event; + if (motionEvent.isTouchEvent()) { + onTouchEvent(motionEvent, nestingLevel); + } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + onTrackballEvent(motionEvent, nestingLevel); + } else { + onGenericMotionEvent(motionEvent, nestingLevel); + } + } + } + + /** + * Checks a key event. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onKeyEvent(KeyEvent event, int nestingLevel) { + if (!startEvent(event, nestingLevel, "KeyEvent")) { + return; + } + + try { + ensureMetaStateIsNormalized(event.getMetaState()); + + final int action = event.getAction(); + final int deviceId = event.getDeviceId(); + final int source = event.getSource(); + final int keyCode = event.getKeyCode(); + switch (action) { + case KeyEvent.ACTION_DOWN: { + KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ false); + if (state != null) { + // If the key is already down, ensure it is a repeat. + // We don't perform this check when processing raw device input + // because the input dispatcher itself is responsible for setting + // the key repeat count before it delivers input events. + if (state.unhandled) { + state.unhandled = false; + } else if ((mFlags & FLAG_RAW_DEVICE_INPUT) == 0 + && event.getRepeatCount() == 0) { + problem("ACTION_DOWN but key is already down and this event " + + "is not a key repeat."); + } + } else { + addKeyState(deviceId, source, keyCode); + } + break; + } + case KeyEvent.ACTION_UP: { + KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ true); + if (state == null) { + problem("ACTION_UP but key was not down."); + } else { + state.recycle(); + } + break; + } + case KeyEvent.ACTION_MULTIPLE: + break; + default: + problem("Invalid action " + KeyEvent.actionToString(action) + + " for key event."); + break; + } + } finally { + finishEvent(false); + } + } + + /** + * Checks a trackball event. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onTrackballEvent(MotionEvent event, int nestingLevel) { + if (!startEvent(event, nestingLevel, "TrackballEvent")) { + return; + } + + try { + ensureMetaStateIsNormalized(event.getMetaState()); + + final int action = event.getAction(); + final int source = event.getSource(); + if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + switch (action) { + case MotionEvent.ACTION_DOWN: + if (mTrackballDown && !mTrackballUnhandled) { + problem("ACTION_DOWN but trackball is already down."); + } else { + mTrackballDown = true; + mTrackballUnhandled = false; + } + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + break; + case MotionEvent.ACTION_UP: + if (!mTrackballDown) { + problem("ACTION_UP but trackball is not down."); + } else { + mTrackballDown = false; + mTrackballUnhandled = false; + } + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + break; + case MotionEvent.ACTION_MOVE: + ensurePointerCountIsOneForThisAction(event); + break; + default: + problem("Invalid action " + MotionEvent.actionToString(action) + + " for trackball event."); + break; + } + + if (mTrackballDown && event.getPressure() <= 0) { + problem("Trackball is down but pressure is not greater than 0."); + } else if (!mTrackballDown && event.getPressure() != 0) { + problem("Trackball is up but pressure is not equal to 0."); + } + } else { + problem("Source was not SOURCE_CLASS_TRACKBALL."); + } + } finally { + finishEvent(false); + } + } + + /** + * Checks a touch event. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onTouchEvent(MotionEvent event, int nestingLevel) { + if (!startEvent(event, nestingLevel, "TouchEvent")) { + return; + } + + final int action = event.getAction(); + final boolean newStream = action == MotionEvent.ACTION_DOWN + || action == MotionEvent.ACTION_CANCEL; + if (mTouchEventStreamIsTainted || mTouchEventStreamUnhandled) { + if (newStream) { + mTouchEventStreamIsTainted = false; + mTouchEventStreamUnhandled = false; + mTouchEventStreamPointers = 0; + } else { + finishEvent(mTouchEventStreamIsTainted); + return; + } + } + + try { + ensureMetaStateIsNormalized(event.getMetaState()); + + final int deviceId = event.getDeviceId(); + final int source = event.getSource(); + + if (!newStream && mTouchEventStreamDeviceId != -1 + && (mTouchEventStreamDeviceId != deviceId + || mTouchEventStreamSource != source)) { + problem("Touch event stream contains events from multiple sources: " + + "previous device id " + mTouchEventStreamDeviceId + + ", previous source " + Integer.toHexString(mTouchEventStreamSource) + + ", new device id " + deviceId + + ", new source " + Integer.toHexString(source)); + } + mTouchEventStreamDeviceId = deviceId; + mTouchEventStreamSource = source; + + final int pointerCount = event.getPointerCount(); + if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (action) { + case MotionEvent.ACTION_DOWN: + if (mTouchEventStreamPointers != 0) { + problem("ACTION_DOWN but pointers are already down. " + + "Probably missing ACTION_UP from previous gesture."); + } + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + mTouchEventStreamPointers = 1 << event.getPointerId(0); + break; + case MotionEvent.ACTION_UP: + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + mTouchEventStreamPointers = 0; + mTouchEventStreamIsTainted = false; + break; + case MotionEvent.ACTION_MOVE: { + final int expectedPointerCount = + Integer.bitCount(mTouchEventStreamPointers); + if (pointerCount != expectedPointerCount) { + problem("ACTION_MOVE contained " + pointerCount + + " pointers but there are currently " + + expectedPointerCount + " pointers down."); + mTouchEventStreamIsTainted = true; + } + break; + } + case MotionEvent.ACTION_CANCEL: + mTouchEventStreamPointers = 0; + mTouchEventStreamIsTainted = false; + break; + case MotionEvent.ACTION_OUTSIDE: + if (mTouchEventStreamPointers != 0) { + problem("ACTION_OUTSIDE but pointers are still down."); + } + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + mTouchEventStreamIsTainted = false; + break; + default: { + final int actionMasked = event.getActionMasked(); + final int actionIndex = event.getActionIndex(); + if (actionMasked == MotionEvent.ACTION_POINTER_DOWN) { + if (mTouchEventStreamPointers == 0) { + problem("ACTION_POINTER_DOWN but no other pointers were down."); + mTouchEventStreamIsTainted = true; + } + if (actionIndex < 0 || actionIndex >= pointerCount) { + problem("ACTION_POINTER_DOWN index is " + actionIndex + + " but the pointer count is " + pointerCount + "."); + mTouchEventStreamIsTainted = true; + } else { + final int id = event.getPointerId(actionIndex); + final int idBit = 1 << id; + if ((mTouchEventStreamPointers & idBit) != 0) { + problem("ACTION_POINTER_DOWN specified pointer id " + id + + " which is already down."); + mTouchEventStreamIsTainted = true; + } else { + mTouchEventStreamPointers |= idBit; + } + } + ensureHistorySizeIsZeroForThisAction(event); + } else if (actionMasked == MotionEvent.ACTION_POINTER_UP) { + if (actionIndex < 0 || actionIndex >= pointerCount) { + problem("ACTION_POINTER_UP index is " + actionIndex + + " but the pointer count is " + pointerCount + "."); + mTouchEventStreamIsTainted = true; + } else { + final int id = event.getPointerId(actionIndex); + final int idBit = 1 << id; + if ((mTouchEventStreamPointers & idBit) == 0) { + problem("ACTION_POINTER_UP specified pointer id " + id + + " which is not currently down."); + mTouchEventStreamIsTainted = true; + } else { + mTouchEventStreamPointers &= ~idBit; + } + } + ensureHistorySizeIsZeroForThisAction(event); + } else { + problem("Invalid action " + MotionEvent.actionToString(action) + + " for touch event."); + } + break; + } + } + } else { + problem("Source was not SOURCE_CLASS_POINTER."); + } + } finally { + finishEvent(false); + } + } + + /** + * Checks a generic motion event. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onGenericMotionEvent(MotionEvent event, int nestingLevel) { + if (!startEvent(event, nestingLevel, "GenericMotionEvent")) { + return; + } + + try { + ensureMetaStateIsNormalized(event.getMetaState()); + + final int action = event.getAction(); + final int source = event.getSource(); + if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (action) { + case MotionEvent.ACTION_HOVER_ENTER: + ensurePointerCountIsOneForThisAction(event); + mHoverEntered = true; + break; + case MotionEvent.ACTION_HOVER_MOVE: + ensurePointerCountIsOneForThisAction(event); + break; + case MotionEvent.ACTION_HOVER_EXIT: + ensurePointerCountIsOneForThisAction(event); + if (!mHoverEntered) { + problem("ACTION_HOVER_EXIT without prior ACTION_HOVER_ENTER"); + } + mHoverEntered = false; + break; + case MotionEvent.ACTION_SCROLL: + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + break; + default: + problem("Invalid action for generic pointer event."); + break; + } + } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { + switch (action) { + case MotionEvent.ACTION_MOVE: + ensurePointerCountIsOneForThisAction(event); + break; + default: + problem("Invalid action for generic joystick event."); + break; + } + } + } finally { + finishEvent(false); + } + } + + /** + * Notifies the verifier that a given event was unhandled and the rest of the + * trace for the event should be ignored. + * This method should only be called if the event was previously checked by + * the consistency verifier using {@link #onInputEvent} and other methods. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onUnhandledEvent(InputEvent event, int nestingLevel) { + if (nestingLevel != mLastNestingLevel) { + return; + } + + if (mRecentEventsUnhandled != null) { + mRecentEventsUnhandled[mMostRecentEventIndex] = true; + } + + if (event instanceof KeyEvent) { + final KeyEvent keyEvent = (KeyEvent)event; + final int deviceId = keyEvent.getDeviceId(); + final int source = keyEvent.getSource(); + final int keyCode = keyEvent.getKeyCode(); + final KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ false); + if (state != null) { + state.unhandled = true; + } + } else { + final MotionEvent motionEvent = (MotionEvent)event; + if (motionEvent.isTouchEvent()) { + mTouchEventStreamUnhandled = true; + } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + if (mTrackballDown) { + mTrackballUnhandled = true; + } + } + } + } + + private void ensureMetaStateIsNormalized(int metaState) { + final int normalizedMetaState = KeyEvent.normalizeMetaState(metaState); + if (normalizedMetaState != metaState) { + problem(String.format("Metastate not normalized. Was 0x%08x but expected 0x%08x.", + metaState, normalizedMetaState)); + } + } + + private void ensurePointerCountIsOneForThisAction(MotionEvent event) { + final int pointerCount = event.getPointerCount(); + if (pointerCount != 1) { + problem("Pointer count is " + pointerCount + " but it should always be 1 for " + + MotionEvent.actionToString(event.getAction())); + } + } + + private void ensureHistorySizeIsZeroForThisAction(MotionEvent event) { + final int historySize = event.getHistorySize(); + if (historySize != 0) { + problem("History size is " + historySize + " but it should always be 0 for " + + MotionEvent.actionToString(event.getAction())); + } + } + + private boolean startEvent(InputEvent event, int nestingLevel, String eventType) { + // Ignore the event if it is already tainted. + if (event.isTainted()) { + return false; + } + + // Ignore the event if we already checked it at a higher nesting level. + if (event == mLastEvent && nestingLevel < mLastNestingLevel) { + return false; + } + + if (nestingLevel > 0) { + mLastEvent = event; + mLastNestingLevel = nestingLevel; + } else { + mLastEvent = null; + mLastNestingLevel = 0; + } + + mCurrentEvent = event; + mCurrentEventType = eventType; + return true; + } + + private void finishEvent(boolean tainted) { + if (mViolationMessage != null && mViolationMessage.length() != 0) { + mViolationMessage.append("\n in ").append(mCaller); + mViolationMessage.append("\n "); + appendEvent(mViolationMessage, 0, mCurrentEvent, false); + + if (RECENT_EVENTS_TO_LOG != 0 && mRecentEvents != null) { + mViolationMessage.append("\n -- recent events --"); + for (int i = 0; i < RECENT_EVENTS_TO_LOG; i++) { + final int index = (mMostRecentEventIndex + RECENT_EVENTS_TO_LOG - i) + % RECENT_EVENTS_TO_LOG; + final InputEvent event = mRecentEvents[index]; + if (event == null) { + break; + } + mViolationMessage.append("\n "); + appendEvent(mViolationMessage, i + 1, event, mRecentEventsUnhandled[index]); + } + } + + Log.d(mLogTag, mViolationMessage.toString()); + mViolationMessage.setLength(0); + tainted = true; + } + + if (tainted) { + // Taint the event so that we do not generate additional violations from it + // further downstream. + mCurrentEvent.setTainted(true); + } + + if (RECENT_EVENTS_TO_LOG != 0) { + if (mRecentEvents == null) { + mRecentEvents = new InputEvent[RECENT_EVENTS_TO_LOG]; + mRecentEventsUnhandled = new boolean[RECENT_EVENTS_TO_LOG]; + } + final int index = (mMostRecentEventIndex + 1) % RECENT_EVENTS_TO_LOG; + mMostRecentEventIndex = index; + if (mRecentEvents[index] != null) { + mRecentEvents[index].recycle(); + } + mRecentEvents[index] = mCurrentEvent.copy(); + mRecentEventsUnhandled[index] = false; + } + + mCurrentEvent = null; + mCurrentEventType = null; + } + + private static void appendEvent(StringBuilder message, int index, + InputEvent event, boolean unhandled) { + message.append(index).append(": sent at ").append(event.getEventTimeNano()); + message.append(", "); + if (unhandled) { + message.append("(unhandled) "); + } + message.append(event); + } + + private void problem(String message) { + if (mViolationMessage == null) { + mViolationMessage = new StringBuilder(); + } + if (mViolationMessage.length() == 0) { + mViolationMessage.append(mCurrentEventType).append(": "); + } else { + mViolationMessage.append("\n "); + } + mViolationMessage.append(message); + } + + private KeyState findKeyState(int deviceId, int source, int keyCode, boolean remove) { + KeyState last = null; + KeyState state = mKeyStateList; + while (state != null) { + if (state.deviceId == deviceId && state.source == source + && state.keyCode == keyCode) { + if (remove) { + if (last != null) { + last.next = state.next; + } else { + mKeyStateList = state.next; + } + state.next = null; + } + return state; + } + last = state; + state = state.next; + } + return null; + } + + private void addKeyState(int deviceId, int source, int keyCode) { + KeyState state = KeyState.obtain(deviceId, source, keyCode); + state.next = mKeyStateList; + mKeyStateList = state; + } + + private static final class KeyState { + private static Object mRecycledListLock = new Object(); + private static KeyState mRecycledList; + + public KeyState next; + public int deviceId; + public int source; + public int keyCode; + public boolean unhandled; + + private KeyState() { + } + + public static KeyState obtain(int deviceId, int source, int keyCode) { + KeyState state; + synchronized (mRecycledListLock) { + state = mRecycledList; + if (state != null) { + mRecycledList = state.next; + } else { + state = new KeyState(); + } + } + state.deviceId = deviceId; + state.source = source; + state.keyCode = keyCode; + state.unhandled = false; + return state; + } + + public void recycle() { + synchronized (mRecycledListLock) { + next = mRecycledList; + mRecycledList = next; + } + } + } +} diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 81d5a6e..5dbda90 100755 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -566,6 +566,19 @@ public class KeyEvent extends InputEvent implements Parcelable { public static final int KEYCODE_BUTTON_15 = 202; /** Key code constant: Generic Game Pad Button #16.*/ public static final int KEYCODE_BUTTON_16 = 203; + /** Key code constant: Language Switch key. + * Toggles the current input language such as switching between English and Japanese on + * a QWERTY keyboard. On some devices, the same function may be performed by + * pressing Shift+Spacebar. */ + public static final int KEYCODE_LANGUAGE_SWITCH = 204; + /** Key code constant: Manner Mode key. + * Toggles silent or vibrate mode on and off to make the device behave more politely + * in certain settings such as on a crowded train. On some devices, the key may only + * operate when long-pressed. */ + public static final int KEYCODE_MANNER_MODE = 205; + /** Key code constant: 3D Mode key. + * Toggles the display between 2D and 3D mode. */ + public static final int KEYCODE_3D_MODE = 206; private static final int LAST_KEYCODE = KEYCODE_BUTTON_16; @@ -791,6 +804,9 @@ public class KeyEvent extends InputEvent implements Parcelable { names.append(KEYCODE_BUTTON_14, "KEYCODE_BUTTON_14"); names.append(KEYCODE_BUTTON_15, "KEYCODE_BUTTON_15"); names.append(KEYCODE_BUTTON_16, "KEYCODE_BUTTON_16"); + names.append(KEYCODE_LANGUAGE_SWITCH, "KEYCODE_LANGUAGE_SWITCH"); + names.append(KEYCODE_MANNER_MODE, "KEYCODE_MANNER_MODE"); + names.append(KEYCODE_3D_MODE, "KEYCODE_3D_MODE"); }; // Symbolic names of all metakeys in bit order from least significant to most significant. @@ -1154,7 +1170,18 @@ public class KeyEvent extends InputEvent implements Parcelable { * @hide */ public static final int FLAG_START_TRACKING = 0x40000000; - + + /** + * Private flag that indicates when the system has detected that this key event + * may be inconsistent with respect to the sequence of previously delivered key events, + * such as when a key up event is sent but the key was not down. + * + * @hide + * @see #isTainted + * @see #setTainted + */ + public static final int FLAG_TAINTED = 0x80000000; + /** * Returns the maximum keycode. */ @@ -1519,6 +1546,33 @@ public class KeyEvent extends InputEvent implements Parcelable { } /** + * Obtains a (potentially recycled) copy of another key event. + * + * @hide + */ + public static KeyEvent obtain(KeyEvent other) { + KeyEvent ev = obtain(); + ev.mDownTime = other.mDownTime; + ev.mEventTime = other.mEventTime; + ev.mAction = other.mAction; + ev.mKeyCode = other.mKeyCode; + ev.mRepeatCount = other.mRepeatCount; + ev.mMetaState = other.mMetaState; + ev.mDeviceId = other.mDeviceId; + ev.mScanCode = other.mScanCode; + ev.mFlags = other.mFlags; + ev.mSource = other.mSource; + ev.mCharacters = other.mCharacters; + return ev; + } + + /** @hide */ + @Override + public KeyEvent copy() { + return obtain(this); + } + + /** * Recycles a key event. * Key events should only be recycled if they are owned by the system since user * code expects them to be essentially immutable, "tracking" notwithstanding. @@ -1619,7 +1673,19 @@ public class KeyEvent extends InputEvent implements Parcelable { event.mFlags = flags; return event; } - + + /** @hide */ + @Override + public final boolean isTainted() { + return (mFlags & FLAG_TAINTED) != 0; + } + + /** @hide */ + @Override + public final void setTainted(boolean tainted) { + mFlags = tainted ? mFlags | FLAG_TAINTED : mFlags & ~FLAG_TAINTED; + } + /** * Don't use in new code, instead explicitly check * {@link #getAction()}. @@ -1741,12 +1807,33 @@ public class KeyEvent extends InputEvent implements Parcelable { * @see #META_CAPS_LOCK_ON * @see #META_NUM_LOCK_ON * @see #META_SCROLL_LOCK_ON + * @see #getModifiers */ public final int getMetaState() { return mMetaState; } /** + * Returns the state of the modifier keys. + * <p> + * For the purposes of this function, {@link #KEYCODE_CAPS_LOCK}, + * {@link #KEYCODE_SCROLL_LOCK}, and {@link #KEYCODE_NUM_LOCK} are + * not considered modifier keys. Consequently, this function specifically masks out + * {@link #META_CAPS_LOCK_ON}, {@link #META_SCROLL_LOCK_ON} and {@link #META_NUM_LOCK_ON}. + * </p><p> + * The value returned consists of the meta state (from {@link #getMetaState}) + * normalized using {@link #normalizeMetaState(int)} and then masked with + * {@link #getModifierMetaStateMask} so that only valid modifier bits are retained. + * </p> + * + * @return An integer in which each bit set to 1 represents a pressed modifier key. + * @see #getMetaState + */ + public final int getModifiers() { + return normalizeMetaState(mMetaState) & META_MODIFIER_MASK; + } + + /** * Returns the flags for this key event. * * @see #FLAG_WOKE_HERE @@ -2253,6 +2340,12 @@ public class KeyEvent extends InputEvent implements Parcelable { return mEventTime; } + /** @hide */ + @Override + public final long getEventTimeNano() { + return mEventTime * 1000000L; + } + /** * Renamed to {@link #getDeviceId}. * @@ -2579,15 +2672,22 @@ public class KeyEvent extends InputEvent implements Parcelable { @Override public String toString() { - return "KeyEvent{action=" + actionToString(mAction) - + " keycode=" + keyCodeToString(mKeyCode) - + " scancode=" + mScanCode - + " metaState=" + metaStateToString(mMetaState) - + " flags=0x" + Integer.toHexString(mFlags) - + " repeat=" + mRepeatCount - + " device=" + mDeviceId - + " source=0x" + Integer.toHexString(mSource) - + "}"; + StringBuilder msg = new StringBuilder(); + msg.append("KeyEvent { action=").append(actionToString(mAction)); + msg.append(", keyCode=").append(keyCodeToString(mKeyCode)); + msg.append(", scanCode=").append(mScanCode); + if (mCharacters != null) { + msg.append(", characters=\"").append(mCharacters).append("\""); + } + msg.append(", metaState=").append(metaStateToString(mMetaState)); + msg.append(", flags=0x").append(Integer.toHexString(mFlags)); + msg.append(", repeatCount=").append(mRepeatCount); + msg.append(", eventTime=").append(mEventTime); + msg.append(", downTime=").append(mDownTime); + msg.append(", deviceId=").append(mDeviceId); + msg.append(", source=0x").append(Integer.toHexString(mSource)); + msg.append(" }"); + return msg.toString(); } /** diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index 81346b4..332a0fa 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -16,6 +16,10 @@ package android.view; +import android.graphics.Canvas; +import android.os.Handler; +import android.os.Message; +import android.widget.FrameLayout; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -83,6 +87,7 @@ public abstract class LayoutInflater { private static final String TAG_MERGE = "merge"; private static final String TAG_INCLUDE = "include"; + private static final String TAG_1995 = "blink"; private static final String TAG_REQUEST_FOCUS = "requestFocus"; /** @@ -454,7 +459,12 @@ public abstract class LayoutInflater { rInflate(parser, root, attrs, false); } else { // Temp is the root view that was found in the xml - View temp = createViewFromTag(root, name, attrs); + View temp; + if (TAG_1995.equals(name)) { + temp = new BlinkLayout(mContext, attrs); + } else { + temp = createViewFromTag(root, name, attrs); + } ViewGroup.LayoutParams params = null; @@ -605,10 +615,9 @@ public abstract class LayoutInflater { * Throw an exception because the specified class is not allowed to be inflated. */ private void failNotAllowed(String name, String prefix, AttributeSet attrs) { - InflateException ie = new InflateException(attrs.getPositionDescription() + throw new InflateException(attrs.getPositionDescription() + ": Class not allowed to be inflated " + (prefix != null ? (prefix + name) : name)); - throw ie; } /** @@ -720,6 +729,12 @@ public abstract class LayoutInflater { parseInclude(parser, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException("<merge /> must be the root element"); + } else if (TAG_1995.equals(name)) { + final View view = new BlinkLayout(mContext, attrs); + final ViewGroup viewGroup = (ViewGroup) parent; + final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); + rInflate(parser, view, attrs, true); + viewGroup.addView(view, params); } else { final View view = createViewFromTag(parent, name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; @@ -847,5 +862,64 @@ public abstract class LayoutInflater { parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) { // Empty } - } + } + + private static class BlinkLayout extends FrameLayout { + private static final int MESSAGE_BLINK = 0x42; + private static final int BLINK_DELAY = 500; + + private boolean mBlink; + private boolean mBlinkState; + private final Handler mHandler; + + public BlinkLayout(Context context, AttributeSet attrs) { + super(context, attrs); + mHandler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + if (msg.what == MESSAGE_BLINK) { + if (mBlink) { + mBlinkState = !mBlinkState; + makeBlink(); + } + invalidate(); + return true; + } + return false; + } + }); + } + + private void makeBlink() { + Message message = mHandler.obtainMessage(MESSAGE_BLINK); + mHandler.sendMessageDelayed(message, BLINK_DELAY); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + mBlink = true; + mBlinkState = true; + + makeBlink(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mBlink = false; + mBlinkState = true; + + mHandler.removeMessages(MESSAGE_BLINK); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (mBlinkState) { + super.dispatchDraw(canvas); + } + } + } } diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index a17db5d..82fd581 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -23,53 +23,60 @@ import android.os.SystemClock; import android.util.SparseArray; /** - * Object used to report movement (mouse, pen, finger, trackball) events. This - * class may hold either absolute or relative movements, depending on what - * it is being used for. + * Object used to report movement (mouse, pen, finger, trackball) events. + * Motion events may hold either absolute or relative movements and other data, + * depending on the type of device. + * + * <h3>Overview</h3> * <p> - * On pointing devices with source class {@link InputDevice#SOURCE_CLASS_POINTER} - * such as touch screens, the pointer coordinates specify absolute - * positions such as view X/Y coordinates. Each complete gesture is represented - * by a sequence of motion events with actions that describe pointer state transitions - * and movements. A gesture starts with a motion event with {@link #ACTION_DOWN} - * that provides the location of the first pointer down. As each additional - * pointer that goes down or up, the framework will generate a motion event with - * {@link #ACTION_POINTER_DOWN} or {@link #ACTION_POINTER_UP} accordingly. - * Pointer movements are described by motion events with {@link #ACTION_MOVE}. - * Finally, a gesture end either when the final pointer goes up as represented - * by a motion event with {@link #ACTION_UP} or when gesture is canceled - * with {@link #ACTION_CANCEL}. + * Motion events describe movements in terms of an action code and a set of axis values. + * The action code specifies the state change that occurred such as a pointer going + * down or up. The axis values describe the position and other movement properties. * </p><p> - * Some pointing devices such as mice may support vertical and/or horizontal scrolling. - * A scroll event is reported as a generic motion event with {@link #ACTION_SCROLL} that - * includes the relative scroll offset in the {@link #AXIS_VSCROLL} and - * {@link #AXIS_HSCROLL} axes. See {@link #getAxisValue(int)} for information - * about retrieving these additional axes. + * For example, when the user first touches the screen, the system delivers a touch + * event to the appropriate {@link View} with the action code {@link #ACTION_DOWN} + * and a set of axis values that include the X and Y coordinates of the touch and + * information about the pressure, size and orientation of the contact area. * </p><p> - * On trackball devices with source class {@link InputDevice#SOURCE_CLASS_TRACKBALL}, - * the pointer coordinates specify relative movements as X/Y deltas. - * A trackball gesture consists of a sequence of movements described by motion - * events with {@link #ACTION_MOVE} interspersed with occasional {@link #ACTION_DOWN} - * or {@link #ACTION_UP} motion events when the trackball button is pressed or released. + * Some devices can report multiple movement traces at the same time. Multi-touch + * screens emit one movement trace for each finger. The individual fingers or + * other objects that generate movement traces are referred to as <em>pointers</em>. + * Motion events contain information about all of the pointers that are currently active + * even if some of them have not moved since the last event was delivered. * </p><p> - * On joystick devices with source class {@link InputDevice#SOURCE_CLASS_JOYSTICK}, - * the pointer coordinates specify the absolute position of the joystick axes. - * The joystick axis values are normalized to a range of -1.0 to 1.0 where 0.0 corresponds - * to the center position. More information about the set of available axes and the - * range of motion can be obtained using {@link InputDevice#getMotionRange}. - * Some common joystick axes are {@link #AXIS_X}, {@link #AXIS_Y}, - * {@link #AXIS_HAT_X}, {@link #AXIS_HAT_Y}, {@link #AXIS_Z} and {@link #AXIS_RZ}. - * </p><p> - * Motion events always report movements for all pointers at once. The number - * of pointers only ever changes by one as individual pointers go up and down, + * The number of pointers only ever changes by one as individual pointers go up and down, * except when the gesture is canceled. * </p><p> - * The order in which individual pointers appear within a motion event can change - * from one event to the next. Use the {@link #getPointerId(int)} method to obtain a - * pointer id to track pointers across motion events in a gesture. Then for - * successive motion events, use the {@link #findPointerIndex(int)} method to obtain - * the pointer index for a given pointer id in that motion event. + * Each pointer has a unique id that is assigned when it first goes down + * (indicated by {@link #ACTION_DOWN} or {@link #ACTION_POINTER_DOWN}). A pointer id + * remains valid until the pointer eventually goes up (indicated by {@link #ACTION_UP} + * or {@link #ACTION_POINTER_UP}) or when the gesture is canceled (indicated by + * {@link #ACTION_CANCEL}). * </p><p> + * The MotionEvent class provides many methods to query the position and other properties of + * pointers, such as {@link #getX(int)}, {@link #getY(int)}, {@link #getAxisValue}, + * {@link #getPointerId(int)}, {@link #getToolType(int)}, and many others. Most of these + * methods accept the pointer index as a parameter rather than the pointer id. + * The pointer index of each pointer in the event ranges from 0 to one less than the value + * returned by {@link #getPointerCount()}. + * </p><p> + * The order in which individual pointers appear within a motion event is undefined. + * Thus the pointer index of a pointer can change from one event to the next but + * the pointer id of a pointer is guaranteed to remain constant as long as the pointer + * remains active. Use the {@link #getPointerId(int)} method to obtain the + * pointer id of a pointer to track it across all subsequent motion events in a gesture. + * Then for successive motion events, use the {@link #findPointerIndex(int)} method + * to obtain the pointer index for a given pointer id in that motion event. + * </p><p> + * Mouse and stylus buttons can be retrieved using {@link #getButtonState()}. It is a + * good idea to check the button state while handling {@link #ACTION_DOWN} as part + * of a touch event. The application may choose to perform some different action + * if the touch event starts due to a secondary button click, such as presenting a + * context menu. + * </p> + * + * <h3>Batching</h3> + * <p> * For efficiency, motion events with {@link #ACTION_MOVE} may batch together * multiple movement samples within a single object. The most current * pointer coordinates are available using {@link #getX(int)} and {@link #getY(int)}. @@ -98,42 +105,104 @@ import android.util.SparseArray; * ev.getPointerId(p), ev.getX(p), ev.getY(p)); * } * } - * </code></pre></p><p> - * In general, the framework cannot guarantee that the motion events it delivers - * to a view always constitute a complete motion sequences since some events may be dropped - * or modified by containing views before they are delivered. The view implementation - * should be prepared to handle {@link #ACTION_CANCEL} and should tolerate anomalous - * situations such as receiving a new {@link #ACTION_DOWN} without first having - * received an {@link #ACTION_UP} for the prior gesture. + * </code></pre></p> + * + * <h3>Device Types</h3> + * <p> + * The interpretation of the contents of a MotionEvent varies significantly depending + * on the source class of the device. + * </p><p> + * On pointing devices with source class {@link InputDevice#SOURCE_CLASS_POINTER} + * such as touch screens, the pointer coordinates specify absolute + * positions such as view X/Y coordinates. Each complete gesture is represented + * by a sequence of motion events with actions that describe pointer state transitions + * and movements. A gesture starts with a motion event with {@link #ACTION_DOWN} + * that provides the location of the first pointer down. As each additional + * pointer that goes down or up, the framework will generate a motion event with + * {@link #ACTION_POINTER_DOWN} or {@link #ACTION_POINTER_UP} accordingly. + * Pointer movements are described by motion events with {@link #ACTION_MOVE}. + * Finally, a gesture end either when the final pointer goes up as represented + * by a motion event with {@link #ACTION_UP} or when gesture is canceled + * with {@link #ACTION_CANCEL}. + * </p><p> + * Some pointing devices such as mice may support vertical and/or horizontal scrolling. + * A scroll event is reported as a generic motion event with {@link #ACTION_SCROLL} that + * includes the relative scroll offset in the {@link #AXIS_VSCROLL} and + * {@link #AXIS_HSCROLL} axes. See {@link #getAxisValue(int)} for information + * about retrieving these additional axes. + * </p><p> + * On trackball devices with source class {@link InputDevice#SOURCE_CLASS_TRACKBALL}, + * the pointer coordinates specify relative movements as X/Y deltas. + * A trackball gesture consists of a sequence of movements described by motion + * events with {@link #ACTION_MOVE} interspersed with occasional {@link #ACTION_DOWN} + * or {@link #ACTION_UP} motion events when the trackball button is pressed or released. + * </p><p> + * On joystick devices with source class {@link InputDevice#SOURCE_CLASS_JOYSTICK}, + * the pointer coordinates specify the absolute position of the joystick axes. + * The joystick axis values are normalized to a range of -1.0 to 1.0 where 0.0 corresponds + * to the center position. More information about the set of available axes and the + * range of motion can be obtained using {@link InputDevice#getMotionRange}. + * Some common joystick axes are {@link #AXIS_X}, {@link #AXIS_Y}, + * {@link #AXIS_HAT_X}, {@link #AXIS_HAT_Y}, {@link #AXIS_Z} and {@link #AXIS_RZ}. * </p><p> * Refer to {@link InputDevice} for more information about how different kinds of * input devices and sources represent pointer coordinates. * </p> + * + * <h3>Consistency Guarantees</h3> + * <p> + * Motion events are always delivered to views as a consistent stream of events. + * What constitutes a consistent stream varies depending on the type of device. + * For touch events, consistency implies that pointers go down one at a time, + * move around as a group and then go up one at a time or are canceled. + * </p><p> + * While the framework tries to deliver consistent streams of motion events to + * views, it cannot guarantee it. Some events may be dropped or modified by + * containing views in the application before they are delivered thereby making + * the stream of events inconsistent. Views should always be prepared to + * handle {@link #ACTION_CANCEL} and should tolerate anomalous + * situations such as receiving a new {@link #ACTION_DOWN} without first having + * received an {@link #ACTION_UP} for the prior gesture. + * </p> */ public final class MotionEvent extends InputEvent implements Parcelable { private static final long NS_PER_MS = 1000000; private static final boolean TRACK_RECYCLED_LOCATION = false; - + + /** + * An invalid pointer id. + * + * This value (-1) can be used as a placeholder to indicate that a pointer id + * has not been assigned or is not available. It cannot appear as + * a pointer id inside a {@link MotionEvent}. + */ + public static final int INVALID_POINTER_ID = -1; + /** * Bit mask of the parts of the action code that are the action itself. */ public static final int ACTION_MASK = 0xff; /** - * Constant for {@link #getAction}: A pressed gesture has started, the + * Constant for {@link #getActionMasked}: A pressed gesture has started, the * motion contains the initial starting location. + * <p> + * This is also a good time to check the button state to distinguish + * secondary and tertiary button clicks and handle them appropriately. + * Use {@link #getButtonState} to retrieve the button state. + * </p> */ public static final int ACTION_DOWN = 0; /** - * Constant for {@link #getAction}: A pressed gesture has finished, the + * Constant for {@link #getActionMasked}: A pressed gesture has finished, the * motion contains the final release location as well as any intermediate * points since the last down or move event. */ public static final int ACTION_UP = 1; /** - * Constant for {@link #getAction}: A change has happened during a + * Constant for {@link #getActionMasked}: A change has happened during a * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}). * The motion contains the most recent point, as well as any intermediate * points since the last down or move event. @@ -141,37 +210,49 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int ACTION_MOVE = 2; /** - * Constant for {@link #getAction}: The current gesture has been aborted. + * Constant for {@link #getActionMasked}: The current gesture has been aborted. * You will not receive any more points in it. You should treat this as * an up event, but not perform any action that you normally would. */ public static final int ACTION_CANCEL = 3; /** - * Constant for {@link #getAction}: A movement has happened outside of the + * Constant for {@link #getActionMasked}: A movement has happened outside of the * normal bounds of the UI element. This does not provide a full gesture, * but only the initial location of the movement/touch. */ public static final int ACTION_OUTSIDE = 4; /** - * A non-primary pointer has gone down. The bits in - * {@link #ACTION_POINTER_ID_MASK} indicate which pointer changed. + * Constant for {@link #getActionMasked}: A non-primary pointer has gone down. + * <p> + * Use {@link #getActionIndex} to retrieve the index of the pointer that changed. + * </p><p> + * The index is encoded in the {@link #ACTION_POINTER_INDEX_MASK} bits of the + * unmasked action returned by {@link #getAction}. + * </p> */ public static final int ACTION_POINTER_DOWN = 5; /** - * A non-primary pointer has gone up. The bits in - * {@link #ACTION_POINTER_ID_MASK} indicate which pointer changed. + * Constant for {@link #getActionMasked}: A non-primary pointer has gone up. + * <p> + * Use {@link #getActionIndex} to retrieve the index of the pointer that changed. + * </p><p> + * The index is encoded in the {@link #ACTION_POINTER_INDEX_MASK} bits of the + * unmasked action returned by {@link #getAction}. + * </p> */ public static final int ACTION_POINTER_UP = 6; /** - * Constant for {@link #getAction}: A change happened but the pointer + * Constant for {@link #getActionMasked}: A change happened but the pointer * is not down (unlike {@link #ACTION_MOVE}). The motion contains the most * recent point, as well as any intermediate points since the last * hover move event. * <p> + * This action is always delivered to the window or view under the pointer. + * </p><p> * This action is not a touch event so it is delivered to * {@link View#onGenericMotionEvent(MotionEvent)} rather than * {@link View#onTouchEvent(MotionEvent)}. @@ -180,13 +261,14 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int ACTION_HOVER_MOVE = 7; /** - * Constant for {@link #getAction}: The motion event contains relative + * Constant for {@link #getActionMasked}: The motion event contains relative * vertical and/or horizontal scroll offsets. Use {@link #getAxisValue(int)} * to retrieve the information from {@link #AXIS_VSCROLL} and {@link #AXIS_HSCROLL}. * The pointer may or may not be down when this event is dispatched. - * This action is always delivered to the winder under the pointer, which - * may not be the window currently touched. * <p> + * This action is always delivered to the window or view under the pointer, which + * may not be the window or view currently touched. + * </p><p> * This action is not a touch event so it is delivered to * {@link View#onGenericMotionEvent(MotionEvent)} rather than * {@link View#onTouchEvent(MotionEvent)}. @@ -195,21 +277,51 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int ACTION_SCROLL = 8; /** + * Constant for {@link #getActionMasked}: The pointer is not down but has entered the + * boundaries of a window or view. + * <p> + * This action is always delivered to the window or view under the pointer. + * </p><p> + * This action is not a touch event so it is delivered to + * {@link View#onGenericMotionEvent(MotionEvent)} rather than + * {@link View#onTouchEvent(MotionEvent)}. + * </p> + */ + public static final int ACTION_HOVER_ENTER = 9; + + /** + * Constant for {@link #getActionMasked}: The pointer is not down but has exited the + * boundaries of a window or view. + * <p> + * This action is always delivered to the window or view that was previously under the pointer. + * </p><p> + * This action is not a touch event so it is delivered to + * {@link View#onGenericMotionEvent(MotionEvent)} rather than + * {@link View#onTouchEvent(MotionEvent)}. + * </p> + */ + public static final int ACTION_HOVER_EXIT = 10; + + /** * Bits in the action code that represent a pointer index, used with * {@link #ACTION_POINTER_DOWN} and {@link #ACTION_POINTER_UP}. Shifting * down by {@link #ACTION_POINTER_INDEX_SHIFT} provides the actual pointer * index where the data for the pointer going up or down can be found; you can * get its identifier with {@link #getPointerId(int)} and the actual * data with {@link #getX(int)} etc. + * + * @see #getActionIndex */ public static final int ACTION_POINTER_INDEX_MASK = 0xff00; /** * Bit shift for the action bits holding the pointer index as * defined by {@link #ACTION_POINTER_INDEX_MASK}. + * + * @see #getActionIndex */ public static final int ACTION_POINTER_INDEX_SHIFT = 8; - + /** * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the * data index associated with {@link #ACTION_POINTER_DOWN}. @@ -279,6 +391,17 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int FLAG_WINDOW_IS_OBSCURED = 0x1; /** + * Private flag that indicates when the system has detected that this motion event + * may be inconsistent with respect to the sequence of previously delivered motion events, + * such as when a pointer move event is sent but the pointer is not down. + * + * @hide + * @see #isTainted + * @see #setTainted + */ + public static final int FLAG_TAINTED = 0x80000000; + + /** * Flag indicating the motion event intersected the top edge of the screen. */ public static final int EDGE_TOP = 0x00000001; @@ -299,7 +422,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int EDGE_RIGHT = 0x00000008; /** - * Constant used to identify the X axis of a motion event. + * Axis constant: X axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the absolute X screen position of the center of @@ -324,7 +447,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_X = 0; /** - * Constant used to identify the Y axis of a motion event. + * Axis constant: Y axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the absolute Y screen position of the center of @@ -349,7 +472,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_Y = 1; /** - * Constant used to identify the Pressure axis of a motion event. + * Axis constant: Pressure axis of a motion event. * <p> * <ul> * <li>For a touch screen or touch pad, reports the approximate pressure applied to the surface @@ -371,7 +494,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_PRESSURE = 2; /** - * Constant used to identify the Size axis of a motion event. + * Axis constant: Size axis of a motion event. * <p> * <ul> * <li>For a touch screen or touch pad, reports the approximate size of the contact area in @@ -391,7 +514,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_SIZE = 3; /** - * Constant used to identify the TouchMajor axis of a motion event. + * Axis constant: TouchMajor axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the length of the major axis of an ellipse that @@ -412,7 +535,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_TOUCH_MAJOR = 4; /** - * Constant used to identify the TouchMinor axis of a motion event. + * Axis constant: TouchMinor axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the length of the minor axis of an ellipse that @@ -435,7 +558,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_TOUCH_MINOR = 5; /** - * Constant used to identify the ToolMajor axis of a motion event. + * Axis constant: ToolMajor axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the length of the major axis of an ellipse that @@ -460,7 +583,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_TOOL_MAJOR = 6; /** - * Constant used to identify the ToolMinor axis of a motion event. + * Axis constant: ToolMinor axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the length of the minor axis of an ellipse that @@ -485,7 +608,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_TOOL_MINOR = 7; /** - * Constant used to identify the Orientation axis of a motion event. + * Axis constant: Orientation axis of a motion event. * <p> * <ul> * <li>For a touch screen or touch pad, reports the orientation of the finger @@ -507,7 +630,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_ORIENTATION = 8; /** - * Constant used to identify the Vertical Scroll axis of a motion event. + * Axis constant: Vertical Scroll axis of a motion event. * <p> * <ul> * <li>For a mouse, reports the relative movement of the vertical scroll wheel. @@ -525,7 +648,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_VSCROLL = 9; /** - * Constant used to identify the Horizontal Scroll axis of a motion event. + * Axis constant: Horizontal Scroll axis of a motion event. * <p> * <ul> * <li>For a mouse, reports the relative movement of the horizontal scroll wheel. @@ -543,7 +666,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_HSCROLL = 10; /** - * Constant used to identify the Z axis of a motion event. + * Axis constant: Z axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute Z position of the joystick. @@ -561,7 +684,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_Z = 11; /** - * Constant used to identify the X Rotation axis of a motion event. + * Axis constant: X Rotation axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute rotation angle about the X axis. @@ -577,7 +700,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_RX = 12; /** - * Constant used to identify the Y Rotation axis of a motion event. + * Axis constant: Y Rotation axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute rotation angle about the Y axis. @@ -593,7 +716,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_RY = 13; /** - * Constant used to identify the Z Rotation axis of a motion event. + * Axis constant: Z Rotation axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute rotation angle about the Z axis. @@ -611,7 +734,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_RZ = 14; /** - * Constant used to identify the Hat X axis of a motion event. + * Axis constant: Hat X axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute X position of the directional hat control. @@ -627,7 +750,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_HAT_X = 15; /** - * Constant used to identify the Hat Y axis of a motion event. + * Axis constant: Hat Y axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute Y position of the directional hat control. @@ -643,7 +766,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_HAT_Y = 16; /** - * Constant used to identify the Left Trigger axis of a motion event. + * Axis constant: Left Trigger axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the left trigger control. @@ -659,7 +782,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_LTRIGGER = 17; /** - * Constant used to identify the Right Trigger axis of a motion event. + * Axis constant: Right Trigger axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the right trigger control. @@ -675,7 +798,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_RTRIGGER = 18; /** - * Constant used to identify the Throttle axis of a motion event. + * Axis constant: Throttle axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the throttle control. @@ -691,7 +814,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_THROTTLE = 19; /** - * Constant used to identify the Rudder axis of a motion event. + * Axis constant: Rudder axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the rudder control. @@ -707,7 +830,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_RUDDER = 20; /** - * Constant used to identify the Wheel axis of a motion event. + * Axis constant: Wheel axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the steering wheel control. @@ -723,7 +846,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_WHEEL = 21; /** - * Constant used to identify the Gas axis of a motion event. + * Axis constant: Gas axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the gas (accelerator) control. @@ -740,7 +863,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GAS = 22; /** - * Constant used to identify the Brake axis of a motion event. + * Axis constant: Brake axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the brake control. @@ -756,7 +879,24 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_BRAKE = 23; /** - * Constant used to identify the Generic 1 axis of a motion event. + * Axis constant: Distance axis of a motion event. + * <p> + * <ul> + * <li>For a stylus, reports the distance of the stylus from the screen. + * The value is normalized to a range from 0.0 (direct contact) to 1.0 (furthest measurable + * distance). + * </ul> + * </p> + * + * @see #getAxisValue(int, int) + * @see #getHistoricalAxisValue(int, int, int) + * @see MotionEvent.PointerCoords#getAxisValue(int) + * @see InputDevice#getMotionRange + */ + public static final int AXIS_DISTANCE = 24; + + /** + * Axis constant: Generic 1 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -767,7 +907,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_1 = 32; /** - * Constant used to identify the Generic 2 axis of a motion event. + * Axis constant: Generic 2 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -778,7 +918,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_2 = 33; /** - * Constant used to identify the Generic 3 axis of a motion event. + * Axis constant: Generic 3 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -789,7 +929,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_3 = 34; /** - * Constant used to identify the Generic 4 axis of a motion event. + * Axis constant: Generic 4 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -800,7 +940,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_4 = 35; /** - * Constant used to identify the Generic 5 axis of a motion event. + * Axis constant: Generic 5 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -811,7 +951,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_5 = 36; /** - * Constant used to identify the Generic 6 axis of a motion event. + * Axis constant: Generic 6 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -822,7 +962,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_6 = 37; /** - * Constant used to identify the Generic 7 axis of a motion event. + * Axis constant: Generic 7 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -833,7 +973,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_7 = 38; /** - * Constant used to identify the Generic 8 axis of a motion event. + * Axis constant: Generic 8 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -844,7 +984,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_8 = 39; /** - * Constant used to identify the Generic 9 axis of a motion event. + * Axis constant: Generic 9 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -855,7 +995,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_9 = 40; /** - * Constant used to identify the Generic 10 axis of a motion event. + * Axis constant: Generic 10 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -866,7 +1006,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_10 = 41; /** - * Constant used to identify the Generic 11 axis of a motion event. + * Axis constant: Generic 11 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -877,7 +1017,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_11 = 42; /** - * Constant used to identify the Generic 12 axis of a motion event. + * Axis constant: Generic 12 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -888,7 +1028,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_12 = 43; /** - * Constant used to identify the Generic 13 axis of a motion event. + * Axis constant: Generic 13 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -899,7 +1039,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_13 = 44; /** - * Constant used to identify the Generic 14 axis of a motion event. + * Axis constant: Generic 14 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -910,7 +1050,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_14 = 45; /** - * Constant used to identify the Generic 15 axis of a motion event. + * Axis constant: Generic 15 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -921,7 +1061,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_15 = 46; /** - * Constant used to identify the Generic 16 axis of a motion event. + * Axis constant: Generic 16 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -937,7 +1077,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { // Symbolic names of all axes. private static final SparseArray<String> AXIS_SYMBOLIC_NAMES = new SparseArray<String>(); - private static void populateAxisSymbolicNames() { + static { SparseArray<String> names = AXIS_SYMBOLIC_NAMES; names.append(AXIS_X, "AXIS_X"); names.append(AXIS_Y, "AXIS_Y"); @@ -963,6 +1103,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { names.append(AXIS_WHEEL, "AXIS_WHEEL"); names.append(AXIS_GAS, "AXIS_GAS"); names.append(AXIS_BRAKE, "AXIS_BRAKE"); + names.append(AXIS_DISTANCE, "AXIS_DISTANCE"); names.append(AXIS_GENERIC_1, "AXIS_GENERIC_1"); names.append(AXIS_GENERIC_2, "AXIS_GENERIC_2"); names.append(AXIS_GENERIC_3, "AXIS_GENERIC_3"); @@ -981,8 +1122,169 @@ public final class MotionEvent extends InputEvent implements Parcelable { names.append(AXIS_GENERIC_16, "AXIS_GENERIC_16"); } + /** + * Button constant: Primary button (left mouse button, stylus tip). + * + * @see #getButtonState + */ + public static final int BUTTON_PRIMARY = 1 << 0; + + /** + * Button constant: Secondary button (right mouse button, stylus barrel). + * + * @see #getButtonState + */ + public static final int BUTTON_SECONDARY = 1 << 1; + + /** + * Button constant: Tertiary button (middle mouse button). + * + * @see #getButtonState + */ + public static final int BUTTON_TERTIARY = 1 << 2; + + /** + * Button constant: Back button pressed (mouse back button). + * <p> + * The system may send a {@link KeyEvent#KEYCODE_BACK} key press to the application + * when this button is pressed. + * </p> + * + * @see #getButtonState + */ + public static final int BUTTON_BACK = 1 << 3; + + /** + * Button constant: Forward button pressed (mouse forward button). + * <p> + * The system may send a {@link KeyEvent#KEYCODE_FORWARD} key press to the application + * when this button is pressed. + * </p> + * + * @see #getButtonState + */ + public static final int BUTTON_FORWARD = 1 << 4; + + /** + * Button constant: Eraser button pressed (stylus end). + * + * @see #getButtonState + */ + public static final int BUTTON_ERASER = 1 << 5; + + // NOTE: If you add a new axis here you must also add it to: + // native/include/android/input.h + + // Symbolic names of all button states in bit order from least significant + // to most significant. + private static final String[] BUTTON_SYMBOLIC_NAMES = new String[] { + "BUTTON_PRIMARY", + "BUTTON_SECONDARY", + "BUTTON_TERTIARY", + "BUTTON_BACK", + "BUTTON_FORWARD", + "BUTTON_ERASER", + "0x00000040", + "0x00000080", + "0x00000100", + "0x00000200", + "0x00000400", + "0x00000800", + "0x00001000", + "0x00002000", + "0x00004000", + "0x00008000", + "0x00010000", + "0x00020000", + "0x00040000", + "0x00080000", + "0x00100000", + "0x00200000", + "0x00400000", + "0x00800000", + "0x01000000", + "0x02000000", + "0x04000000", + "0x08000000", + "0x10000000", + "0x20000000", + "0x40000000", + "0x80000000", + }; + + /** + * Tool type constant: Unknown tool type. + * This constant is used when the tool type is not known or is not relevant, + * such as for a trackball or other non-pointing device. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_UNKNOWN = 0; + + /** + * Tool type constant: The tool is a finger directly touching the display. + * + * This is a <em>direct</em> positioning tool. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_FINGER = 1; + + /** + * Tool type constant: The tool is a stylus directly touching the display + * or hovering slightly above it. + * + * This is a <em>direct</em> positioning tool. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_STYLUS = 2; + + /** + * Tool type constant: The tool is a mouse or trackpad that translates + * relative motions into cursor movements on the display. + * + * This is an <em>indirect</em> positioning tool. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_MOUSE = 3; + + /** + * Tool type constant: The tool is a finger on a touch pad that is not + * directly attached to the display. Finger movements on the touch pad + * may be translated into touches on the display, possibly with visual feedback. + * + * This is an <em>indirect</em> positioning tool. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_INDIRECT_FINGER = 4; + + /** + * Tool type constant: The tool is a stylus on a digitizer tablet that is not + * attached to the display. Stylus movements on the digitizer may be translated + * into touches on the display, possibly with visual feedback. + * + * This is an <em>indirect</em> positioning tool. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_INDIRECT_STYLUS = 5; + + // NOTE: If you add a new tool type here you must also add it to: + // native/include/android/input.h + + // Symbolic names of all tool types. + private static final SparseArray<String> TOOL_TYPE_SYMBOLIC_NAMES = new SparseArray<String>(); static { - populateAxisSymbolicNames(); + SparseArray<String> names = TOOL_TYPE_SYMBOLIC_NAMES; + names.append(TOOL_TYPE_UNKNOWN, "TOOL_TYPE_UNKNOWN"); + names.append(TOOL_TYPE_FINGER, "TOOL_TYPE_FINGER"); + names.append(TOOL_TYPE_STYLUS, "TOOL_TYPE_STYLUS"); + names.append(TOOL_TYPE_MOUSE, "TOOL_TYPE_MOUSE"); + names.append(TOOL_TYPE_INDIRECT_FINGER, "TOOL_TYPE_INDIRECT_FINGER"); + names.append(TOOL_TYPE_INDIRECT_STYLUS, "TOOL_TYPE_INDIRECT_STYLUS"); } // Private value for history pos that obtains the current sample. @@ -995,10 +1297,23 @@ public final class MotionEvent extends InputEvent implements Parcelable { // Shared temporary objects used when translating coordinates supplied by // the caller into single element PointerCoords and pointer id arrays. - // Must lock gTmpPointerCoords prior to use. - private static final PointerCoords[] gTmpPointerCoords = - new PointerCoords[] { new PointerCoords() }; - private static final int[] gTmpPointerIds = new int[] { 0 /*always 0*/ }; + private static final Object gSharedTempLock = new Object(); + private static PointerCoords[] gSharedTempPointerCoords; + private static PointerProperties[] gSharedTempPointerProperties; + private static int[] gSharedTempPointerIndexMap; + + private static final void ensureSharedTempPointerCapacity(int desiredCapacity) { + if (gSharedTempPointerCoords == null + || gSharedTempPointerCoords.length < desiredCapacity) { + int capacity = gSharedTempPointerCoords != null ? gSharedTempPointerCoords.length : 8; + while (capacity < desiredCapacity) { + capacity *= 2; + } + gSharedTempPointerCoords = PointerCoords.createArray(capacity); + gSharedTempPointerProperties = PointerProperties.createArray(capacity); + gSharedTempPointerIndexMap = new int[capacity]; + } + } // Pointer to the native MotionEvent object that contains the actual data. private int mNativePtr; @@ -1008,10 +1323,11 @@ public final class MotionEvent extends InputEvent implements Parcelable { private boolean mRecycled; private static native int nativeInitialize(int nativePtr, - int deviceId, int source, int action, int flags, int edgeFlags, int metaState, + int deviceId, int source, int action, int flags, int edgeFlags, + int metaState, int buttonState, float xOffset, float yOffset, float xPrecision, float yPrecision, long downTimeNanos, long eventTimeNanos, - int pointerCount, int[] pointerIds, PointerCoords[] pointerCoords); + int pointerCount, PointerProperties[] pointerIds, PointerCoords[] pointerCoords); private static native int nativeCopy(int destNativePtr, int sourceNativePtr, boolean keepHistory); private static native void nativeDispose(int nativePtr); @@ -1025,16 +1341,22 @@ public final class MotionEvent extends InputEvent implements Parcelable { private static native void nativeSetAction(int nativePtr, int action); private static native boolean nativeIsTouchEvent(int nativePtr); private static native int nativeGetFlags(int nativePtr); + private static native void nativeSetFlags(int nativePtr, int flags); private static native int nativeGetEdgeFlags(int nativePtr); private static native void nativeSetEdgeFlags(int nativePtr, int action); private static native int nativeGetMetaState(int nativePtr); + private static native int nativeGetButtonState(int nativePtr); private static native void nativeOffsetLocation(int nativePtr, float deltaX, float deltaY); + private static native float nativeGetXOffset(int nativePtr); + private static native float nativeGetYOffset(int nativePtr); private static native float nativeGetXPrecision(int nativePtr); private static native float nativeGetYPrecision(int nativePtr); private static native long nativeGetDownTimeNanos(int nativePtr); + private static native void nativeSetDownTimeNanos(int nativePtr, long downTime); private static native int nativeGetPointerCount(int nativePtr); private static native int nativeGetPointerId(int nativePtr, int pointerIndex); + private static native int nativeGetToolType(int nativePtr, int pointerIndex); private static native int nativeFindPointerIndex(int nativePtr, int pointerId); private static native int nativeGetHistorySize(int nativePtr); @@ -1045,6 +1367,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { int axis, int pointerIndex, int historyPos); private static native void nativeGetPointerCoords(int nativePtr, int pointerIndex, int historyPos, PointerCoords outPointerCoords); + private static native void nativeGetPointerProperties(int nativePtr, + int pointerIndex, PointerProperties outPointerProperties); private static native void nativeScale(int nativePtr, float scale); private static native void nativeTransform(int nativePtr, Matrix matrix); @@ -1086,19 +1410,21 @@ public final class MotionEvent extends InputEvent implements Parcelable { /** * Create a new MotionEvent, filling in all of the basic values that * define the motion. - * + * * @param downTime The time (in ms) when the user originally pressed down to start * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}. * @param eventTime The the time (in ms) when this specific event was generated. This * must be obtained from {@link SystemClock#uptimeMillis()}. * @param action The kind of action being performed, such as {@link #ACTION_DOWN}. - * @param pointers The number of points that will be in this event. - * @param pointerIds An array of <em>pointers</em> values providing - * an identifier for each pointer. - * @param pointerCoords An array of <em>pointers</em> values providing + * @param pointerCount The number of pointers that will be in this event. + * @param pointerProperties An array of <em>pointerCount</em> values providing + * a {@link PointerProperties} property object for each pointer, which must + * include the pointer identifier. + * @param pointerCoords An array of <em>pointerCount</em> values providing * a {@link PointerCoords} coordinate object for each pointer. * @param metaState The state of any meta / modifier keys that were in effect when * the event was generated. + * @param buttonState The state of buttons that are pressed. * @param xPrecision The precision of the X coordinate being reported. * @param yPrecision The precision of the Y coordinate being reported. * @param deviceId The id for the device that this event came from. An id of @@ -1110,21 +1436,69 @@ public final class MotionEvent extends InputEvent implements Parcelable { * @param flags The motion event flags. */ static public MotionEvent obtain(long downTime, long eventTime, - int action, int pointers, int[] pointerIds, PointerCoords[] pointerCoords, - int metaState, float xPrecision, float yPrecision, int deviceId, + int action, int pointerCount, PointerProperties[] pointerProperties, + PointerCoords[] pointerCoords, int metaState, int buttonState, + float xPrecision, float yPrecision, int deviceId, int edgeFlags, int source, int flags) { MotionEvent ev = obtain(); ev.mNativePtr = nativeInitialize(ev.mNativePtr, - deviceId, source, action, flags, edgeFlags, metaState, + deviceId, source, action, flags, edgeFlags, metaState, buttonState, 0, 0, xPrecision, yPrecision, downTime * NS_PER_MS, eventTime * NS_PER_MS, - pointers, pointerIds, pointerCoords); + pointerCount, pointerProperties, pointerCoords); return ev; } /** * Create a new MotionEvent, filling in all of the basic values that * define the motion. + * + * @param downTime The time (in ms) when the user originally pressed down to start + * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}. + * @param eventTime The the time (in ms) when this specific event was generated. This + * must be obtained from {@link SystemClock#uptimeMillis()}. + * @param action The kind of action being performed, such as {@link #ACTION_DOWN}. + * @param pointerCount The number of pointers that will be in this event. + * @param pointerIds An array of <em>pointerCount</em> values providing + * an identifier for each pointer. + * @param pointerCoords An array of <em>pointerCount</em> values providing + * a {@link PointerCoords} coordinate object for each pointer. + * @param metaState The state of any meta / modifier keys that were in effect when + * the event was generated. + * @param xPrecision The precision of the X coordinate being reported. + * @param yPrecision The precision of the Y coordinate being reported. + * @param deviceId The id for the device that this event came from. An id of + * zero indicates that the event didn't come from a physical device; other + * numbers are arbitrary and you shouldn't depend on the values. + * @param edgeFlags A bitfield indicating which edges, if any, were touched by this + * MotionEvent. + * @param source The source of this event. + * @param flags The motion event flags. + * + * @deprecated Use {@link #obtain(long, long, int, int, PointerProperties[], PointerCoords[], int, int, float, float, int, int, int, int)} + * instead. + */ + @Deprecated + static public MotionEvent obtain(long downTime, long eventTime, + int action, int pointerCount, int[] pointerIds, PointerCoords[] pointerCoords, + int metaState, float xPrecision, float yPrecision, int deviceId, + int edgeFlags, int source, int flags) { + synchronized (gSharedTempLock) { + ensureSharedTempPointerCapacity(pointerCount); + final PointerProperties[] pp = gSharedTempPointerProperties; + for (int i = 0; i < pointerCount; i++) { + pp[i].clear(); + pp[i].id = pointerIds[i]; + } + return obtain(downTime, eventTime, action, pointerCount, pp, + pointerCoords, metaState, 0, xPrecision, yPrecision, deviceId, + edgeFlags, source, flags); + } + } + + /** + * Create a new MotionEvent, filling in all of the basic values that + * define the motion. * * @param downTime The time (in ms) when the user originally pressed down to start * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}. @@ -1154,20 +1528,25 @@ public final class MotionEvent extends InputEvent implements Parcelable { static public MotionEvent obtain(long downTime, long eventTime, int action, float x, float y, float pressure, float size, int metaState, float xPrecision, float yPrecision, int deviceId, int edgeFlags) { - synchronized (gTmpPointerCoords) { - final PointerCoords pc = gTmpPointerCoords[0]; - pc.clear(); - pc.x = x; - pc.y = y; - pc.pressure = pressure; - pc.size = size; - - MotionEvent ev = obtain(); + MotionEvent ev = obtain(); + synchronized (gSharedTempLock) { + ensureSharedTempPointerCapacity(1); + final PointerProperties[] pp = gSharedTempPointerProperties; + pp[0].clear(); + pp[0].id = 0; + + final PointerCoords pc[] = gSharedTempPointerCoords; + pc[0].clear(); + pc[0].x = x; + pc[0].y = y; + pc[0].pressure = pressure; + pc[0].size = size; + ev.mNativePtr = nativeInitialize(ev.mNativePtr, - deviceId, InputDevice.SOURCE_UNKNOWN, action, 0, edgeFlags, metaState, + deviceId, InputDevice.SOURCE_UNKNOWN, action, 0, edgeFlags, metaState, 0, 0, 0, xPrecision, yPrecision, downTime * NS_PER_MS, eventTime * NS_PER_MS, - 1, gTmpPointerIds, gTmpPointerCoords); + 1, pp, pc); return ev; } } @@ -1181,7 +1560,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { * @param eventTime The the time (in ms) when this specific event was generated. This * must be obtained from {@link SystemClock#uptimeMillis()}. * @param action The kind of action being performed, such as {@link #ACTION_DOWN}. - * @param pointers The number of pointers that are active in this event. + * @param pointerCount The number of pointers that are active in this event. * @param x The X coordinate of this event. * @param y The Y coordinate of this event. * @param pressure The current pressure of this event. The pressure generally @@ -1207,7 +1586,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { */ @Deprecated static public MotionEvent obtain(long downTime, long eventTime, int action, - int pointers, float x, float y, float pressure, float size, int metaState, + int pointerCount, float x, float y, float pressure, float size, int metaState, float xPrecision, float yPrecision, int deviceId, int edgeFlags) { return obtain(downTime, eventTime, action, x, y, pressure, size, metaState, xPrecision, yPrecision, deviceId, edgeFlags); @@ -1261,6 +1640,12 @@ public final class MotionEvent extends InputEvent implements Parcelable { return ev; } + /** @hide */ + @Override + public MotionEvent copy() { + return obtain(this); + } + /** * Recycle the MotionEvent, to be re-used by a later caller. After calling * this function you must not ever touch the event again. @@ -1354,9 +1739,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { /** * Returns true if this motion event is a touch event. * <p> - * Specifically excludes pointer events with action {@link #ACTION_HOVER_MOVE} - * or {@link #ACTION_SCROLL} because they are not actually touch events - * (the pointer is not down). + * Specifically excludes pointer events with action {@link #ACTION_HOVER_MOVE}, + * {@link #ACTION_HOVER_ENTER}, {@link #ACTION_HOVER_EXIT}, or {@link #ACTION_SCROLL} + * because they are not actually touch events (the pointer is not down). * </p> * @return True if this motion event is a touch event. * @hide @@ -1374,6 +1759,20 @@ public final class MotionEvent extends InputEvent implements Parcelable { return nativeGetFlags(mNativePtr); } + /** @hide */ + @Override + public final boolean isTainted() { + final int flags = getFlags(); + return (flags & FLAG_TAINTED) != 0; + } + + /** @hide */ + @Override + public final void setTainted(boolean tainted) { + final int flags = getFlags(); + nativeSetFlags(mNativePtr, tainted ? flags | FLAG_TAINTED : flags & ~FLAG_TAINTED); + } + /** * Returns the time (in ms) when the user originally pressed down to start * a stream of position events. @@ -1383,6 +1782,16 @@ public final class MotionEvent extends InputEvent implements Parcelable { } /** + * Sets the time (in ms) when the user originally pressed down to start + * a stream of position events. + * + * @hide + */ + public final void setDownTime(long downTime) { + nativeSetDownTimeNanos(mNativePtr, downTime * NS_PER_MS); + } + + /** * Returns the time (in ms) when this specific event was generated. */ public final long getEventTime() { @@ -1521,7 +1930,27 @@ public final class MotionEvent extends InputEvent implements Parcelable { public final int getPointerId(int pointerIndex) { return nativeGetPointerId(mNativePtr, pointerIndex); } - + + /** + * Gets the tool type of a pointer for the given pointer index. + * The tool type indicates the type of tool used to make contact such + * as a finger or stylus, if known. + * + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. + * @return The tool type of the pointer. + * + * @see #TOOL_TYPE_UNKNOWN + * @see #TOOL_TYPE_FINGER + * @see #TOOL_TYPE_STYLUS + * @see #TOOL_TYPE_MOUSE + * @see #TOOL_TYPE_INDIRECT_FINGER + * @see #TOOL_TYPE_INDIRECT_STYLUS + */ + public final int getToolType(int pointerIndex) { + return nativeGetToolType(mNativePtr, pointerIndex); + } + /** * Given a pointer identifier, find the index of its data in the event. * @@ -1533,7 +1962,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public final int findPointerIndex(int pointerId) { return nativeFindPointerIndex(mNativePtr, pointerId); } - + /** * Returns the X coordinate of this event for the given pointer * <em>index</em> (use {@link #getPointerId(int)} to find the pointer @@ -1709,6 +2138,21 @@ public final class MotionEvent extends InputEvent implements Parcelable { } /** + * Populates a {@link PointerProperties} object with pointer properties for + * the specified pointer index. + * + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. + * @param outPointerProperties The pointer properties object to populate. + * + * @see PointerProperties + */ + public final void getPointerProperties(int pointerIndex, + PointerProperties outPointerProperties) { + nativeGetPointerProperties(mNativePtr, pointerIndex, outPointerProperties); + } + + /** * Returns the state of any meta / modifier keys that were in effect when * the event was generated. This is the same values as those * returned by {@link KeyEvent#getMetaState() KeyEvent.getMetaState}. @@ -1723,6 +2167,22 @@ public final class MotionEvent extends InputEvent implements Parcelable { } /** + * Gets the state of all buttons that are pressed such as a mouse or stylus button. + * + * @return The button state. + * + * @see #BUTTON_PRIMARY + * @see #BUTTON_SECONDARY + * @see #BUTTON_TERTIARY + * @see #BUTTON_FORWARD + * @see #BUTTON_BACK + * @see #BUTTON_ERASER + */ + public final int getButtonState() { + return nativeGetButtonState(mNativePtr); + } + + /** * Returns the original raw X coordinate of this event. For touch * events on the screen, this is the original location of the event * on the screen, before it had been adjusted for the containing window @@ -2236,14 +2696,16 @@ public final class MotionEvent extends InputEvent implements Parcelable { */ public final void addBatch(long eventTime, float x, float y, float pressure, float size, int metaState) { - synchronized (gTmpPointerCoords) { - final PointerCoords pc = gTmpPointerCoords[0]; - pc.clear(); - pc.x = x; - pc.y = y; - pc.pressure = pressure; - pc.size = size; - nativeAddBatch(mNativePtr, eventTime * NS_PER_MS, gTmpPointerCoords, metaState); + synchronized (gSharedTempLock) { + ensureSharedTempPointerCapacity(1); + final PointerCoords[] pc = gSharedTempPointerCoords; + pc[0].clear(); + pc[0].x = x; + pc[0].y = y; + pc[0].pressure = pressure; + pc[0].size = size; + + nativeAddBatch(mNativePtr, eventTime * NS_PER_MS, pc, metaState); } } @@ -2262,30 +2724,187 @@ public final class MotionEvent extends InputEvent implements Parcelable { nativeAddBatch(mNativePtr, eventTime * NS_PER_MS, pointerCoords, metaState); } + /** + * Returns true if all points in the motion event are completely within the specified bounds. + * @hide + */ + public final boolean isWithinBoundsNoHistory(float left, float top, + float right, float bottom) { + final int pointerCount = nativeGetPointerCount(mNativePtr); + for (int i = 0; i < pointerCount; i++) { + final float x = nativeGetAxisValue(mNativePtr, AXIS_X, i, HISTORY_CURRENT); + final float y = nativeGetAxisValue(mNativePtr, AXIS_Y, i, HISTORY_CURRENT); + if (x < left || x > right || y < top || y > bottom) { + return false; + } + } + return true; + } + + private static final float clamp(float value, float low, float high) { + if (value < low) { + return low; + } else if (value > high) { + return high; + } + return value; + } + + /** + * Returns a new motion events whose points have been clamped to the specified bounds. + * @hide + */ + public final MotionEvent clampNoHistory(float left, float top, float right, float bottom) { + MotionEvent ev = obtain(); + synchronized (gSharedTempLock) { + final int pointerCount = nativeGetPointerCount(mNativePtr); + + ensureSharedTempPointerCapacity(pointerCount); + final PointerProperties[] pp = gSharedTempPointerProperties; + final PointerCoords[] pc = gSharedTempPointerCoords; + + for (int i = 0; i < pointerCount; i++) { + nativeGetPointerProperties(mNativePtr, i, pp[i]); + nativeGetPointerCoords(mNativePtr, i, HISTORY_CURRENT, pc[i]); + pc[i].x = clamp(pc[i].x, left, right); + pc[i].y = clamp(pc[i].y, top, bottom); + } + ev.mNativePtr = nativeInitialize(ev.mNativePtr, + nativeGetDeviceId(mNativePtr), nativeGetSource(mNativePtr), + nativeGetAction(mNativePtr), nativeGetFlags(mNativePtr), + nativeGetEdgeFlags(mNativePtr), nativeGetMetaState(mNativePtr), + nativeGetButtonState(mNativePtr), + nativeGetXOffset(mNativePtr), nativeGetYOffset(mNativePtr), + nativeGetXPrecision(mNativePtr), nativeGetYPrecision(mNativePtr), + nativeGetDownTimeNanos(mNativePtr), + nativeGetEventTimeNanos(mNativePtr, HISTORY_CURRENT), + pointerCount, pp, pc); + return ev; + } + } + + /** + * Gets an integer where each pointer id present in the event is marked as a bit. + * @hide + */ + public final int getPointerIdBits() { + int idBits = 0; + final int pointerCount = nativeGetPointerCount(mNativePtr); + for (int i = 0; i < pointerCount; i++) { + idBits |= 1 << nativeGetPointerId(mNativePtr, i); + } + return idBits; + } + + /** + * Splits a motion event such that it includes only a subset of pointer ids. + * @hide + */ + public final MotionEvent split(int idBits) { + MotionEvent ev = obtain(); + synchronized (gSharedTempLock) { + final int oldPointerCount = nativeGetPointerCount(mNativePtr); + ensureSharedTempPointerCapacity(oldPointerCount); + final PointerProperties[] pp = gSharedTempPointerProperties; + final PointerCoords[] pc = gSharedTempPointerCoords; + final int[] map = gSharedTempPointerIndexMap; + + final int oldAction = nativeGetAction(mNativePtr); + final int oldActionMasked = oldAction & ACTION_MASK; + final int oldActionPointerIndex = (oldAction & ACTION_POINTER_INDEX_MASK) + >> ACTION_POINTER_INDEX_SHIFT; + int newActionPointerIndex = -1; + int newPointerCount = 0; + int newIdBits = 0; + for (int i = 0; i < oldPointerCount; i++) { + nativeGetPointerProperties(mNativePtr, i, pp[newPointerCount]); + final int idBit = 1 << pp[newPointerCount].id; + if ((idBit & idBits) != 0) { + if (i == oldActionPointerIndex) { + newActionPointerIndex = newPointerCount; + } + map[newPointerCount] = i; + newPointerCount += 1; + newIdBits |= idBit; + } + } + + if (newPointerCount == 0) { + throw new IllegalArgumentException("idBits did not match any ids in the event"); + } + + final int newAction; + if (oldActionMasked == ACTION_POINTER_DOWN || oldActionMasked == ACTION_POINTER_UP) { + if (newActionPointerIndex < 0) { + // An unrelated pointer changed. + newAction = ACTION_MOVE; + } else if (newPointerCount == 1) { + // The first/last pointer went down/up. + newAction = oldActionMasked == ACTION_POINTER_DOWN + ? ACTION_DOWN : ACTION_UP; + } else { + // A secondary pointer went down/up. + newAction = oldActionMasked + | (newActionPointerIndex << ACTION_POINTER_INDEX_SHIFT); + } + } else { + // Simple up/down/cancel/move or other motion action. + newAction = oldAction; + } + + final int historySize = nativeGetHistorySize(mNativePtr); + for (int h = 0; h <= historySize; h++) { + final int historyPos = h == historySize ? HISTORY_CURRENT : h; + + for (int i = 0; i < newPointerCount; i++) { + nativeGetPointerCoords(mNativePtr, map[i], historyPos, pc[i]); + } + + final long eventTimeNanos = nativeGetEventTimeNanos(mNativePtr, historyPos); + if (h == 0) { + ev.mNativePtr = nativeInitialize(ev.mNativePtr, + nativeGetDeviceId(mNativePtr), nativeGetSource(mNativePtr), + newAction, nativeGetFlags(mNativePtr), + nativeGetEdgeFlags(mNativePtr), nativeGetMetaState(mNativePtr), + nativeGetButtonState(mNativePtr), + nativeGetXOffset(mNativePtr), nativeGetYOffset(mNativePtr), + nativeGetXPrecision(mNativePtr), nativeGetYPrecision(mNativePtr), + nativeGetDownTimeNanos(mNativePtr), eventTimeNanos, + newPointerCount, pp, pc); + } else { + nativeAddBatch(ev.mNativePtr, eventTimeNanos, pc, 0); + } + } + return ev; + } + } + @Override public String toString() { - return "MotionEvent{" + Integer.toHexString(System.identityHashCode(this)) - + " pointerId=" + getPointerId(0) - + " action=" + actionToString(getAction()) - + " x=" + getX() - + " y=" + getY() - + " pressure=" + getPressure() - + " size=" + getSize() - + " touchMajor=" + getTouchMajor() - + " touchMinor=" + getTouchMinor() - + " toolMajor=" + getToolMajor() - + " toolMinor=" + getToolMinor() - + " orientation=" + getOrientation() - + " meta=" + KeyEvent.metaStateToString(getMetaState()) - + " pointerCount=" + getPointerCount() - + " historySize=" + getHistorySize() - + " flags=0x" + Integer.toHexString(getFlags()) - + " edgeFlags=0x" + Integer.toHexString(getEdgeFlags()) - + " device=" + getDeviceId() - + " source=0x" + Integer.toHexString(getSource()) - + (getPointerCount() > 1 ? - " pointerId2=" + getPointerId(1) + " x2=" + getX(1) + " y2=" + getY(1) : "") - + "}"; + StringBuilder msg = new StringBuilder(); + msg.append("MotionEvent { action=").append(actionToString(getAction())); + + final int pointerCount = getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + msg.append(", id[").append(i).append("]=").append(getPointerId(i)); + msg.append(", x[").append(i).append("]=").append(getX(i)); + msg.append(", y[").append(i).append("]=").append(getY(i)); + msg.append(", toolType[").append(i).append("]=").append( + toolTypeToString(getToolType(i))); + } + + msg.append(", buttonState=").append(KeyEvent.metaStateToString(getButtonState())); + msg.append(", metaState=").append(KeyEvent.metaStateToString(getMetaState())); + msg.append(", flags=0x").append(Integer.toHexString(getFlags())); + msg.append(", edgeFlags=0x").append(Integer.toHexString(getEdgeFlags())); + msg.append(", pointerCount=").append(pointerCount); + msg.append(", historySize=").append(getHistorySize()); + msg.append(", eventTime=").append(getEventTime()); + msg.append(", downTime=").append(getDownTime()); + msg.append(", deviceId=").append(getDeviceId()); + msg.append(", source=0x").append(Integer.toHexString(getSource())); + msg.append(" }"); + return msg.toString(); } /** @@ -2313,6 +2932,10 @@ public final class MotionEvent extends InputEvent implements Parcelable { return "ACTION_HOVER_MOVE"; case ACTION_SCROLL: return "ACTION_SCROLL"; + case ACTION_HOVER_ENTER: + return "ACTION_HOVER_ENTER"; + case ACTION_HOVER_EXIT: + return "ACTION_HOVER_EXIT"; } int index = (action & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT; switch (action & ACTION_MASK) { @@ -2364,6 +2987,55 @@ public final class MotionEvent extends InputEvent implements Parcelable { } } + /** + * Returns a string that represents the symbolic name of the specified combined + * button state flags such as "0", "BUTTON_PRIMARY", + * "BUTTON_PRIMARY|BUTTON_SECONDARY" or an equivalent numeric constant such as "0x10000000" + * if unknown. + * + * @param buttonState The button state. + * @return The symbolic name of the specified combined button state flags. + * @hide + */ + public static String buttonStateToString(int buttonState) { + if (buttonState == 0) { + return "0"; + } + StringBuilder result = null; + int i = 0; + while (buttonState != 0) { + final boolean isSet = (buttonState & 1) != 0; + buttonState >>>= 1; // unsigned shift! + if (isSet) { + final String name = BUTTON_SYMBOLIC_NAMES[i]; + if (result == null) { + if (buttonState == 0) { + return name; + } + result = new StringBuilder(name); + } else { + result.append('|'); + result.append(name); + } + } + i += 1; + } + return result.toString(); + } + + /** + * Returns a string that represents the symbolic name of the specified tool type + * such as "TOOL_TYPE_FINGER" or an equivalent numeric constant such as "42" if unknown. + * + * @param toolType The tool type. + * @return The symbolic name of the specified tool type. + * @hide + */ + public static String toolTypeToString(int toolType) { + String symbolicName = TOOL_TYPE_SYMBOLIC_NAMES.get(toolType); + return symbolicName != null ? symbolicName : Integer.toString(toolType); + } + public static final Parcelable.Creator<MotionEvent> CREATOR = new Parcelable.Creator<MotionEvent>() { public MotionEvent createFromParcel(Parcel in) { @@ -2391,8 +3063,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { /** * Transfer object for pointer coordinates. * - * Objects of this type can be used to manufacture new {@link MotionEvent} objects - * and to query pointer coordinate information in bulk. + * Objects of this type can be used to specify the pointer coordinates when + * creating new {@link MotionEvent} objects and to query pointer coordinates + * in bulk. * * Refer to {@link InputDevice} for information about how different kinds of * input devices and sources represent pointer coordinates. @@ -2418,6 +3091,15 @@ public final class MotionEvent extends InputEvent implements Parcelable { copyFrom(other); } + /** @hide */ + public static PointerCoords[] createArray(int size) { + PointerCoords[] array = new PointerCoords[size]; + for (int i = 0; i < size; i++) { + array[i] = new PointerCoords(); + } + return array; + } + /** * The X component of the pointer movement. * @@ -2678,4 +3360,71 @@ public final class MotionEvent extends InputEvent implements Parcelable { } } } + + /** + * Transfer object for pointer properties. + * + * Objects of this type can be used to specify the pointer id and tool type + * when creating new {@link MotionEvent} objects and to query pointer properties in bulk. + */ + public static final class PointerProperties { + /** + * Creates a pointer properties object with an invalid pointer id. + */ + public PointerProperties() { + clear(); + } + + /** + * Creates a pointer properties object as a copy of the contents of + * another pointer properties object. + * @param other + */ + public PointerProperties(PointerProperties other) { + copyFrom(other); + } + + /** @hide */ + public static PointerProperties[] createArray(int size) { + PointerProperties[] array = new PointerProperties[size]; + for (int i = 0; i < size; i++) { + array[i] = new PointerProperties(); + } + return array; + } + + /** + * The pointer id. + * Initially set to {@link #INVALID_POINTER_ID} (-1). + * + * @see MotionEvent#getPointerId(int) + */ + public int id; + + /** + * The pointer tool type. + * Initially set to 0. + * + * @see MotionEvent#getToolType(int) + */ + public int toolType; + + /** + * Resets the pointer properties to their initial values. + */ + public void clear() { + id = INVALID_POINTER_ID; + toolType = TOOL_TYPE_UNKNOWN; + } + + /** + * Copies the contents of another pointer properties object. + * + * @param other The pointer properties object to copy. + */ + public void copyFrom(PointerProperties other) { + id = other.id; + toolType = other.toolType; + } + } } diff --git a/core/java/android/view/OrientationEventListener.java b/core/java/android/view/OrientationEventListener.java index 391ba1e..cd48a4f 100755 --- a/core/java/android/view/OrientationEventListener.java +++ b/core/java/android/view/OrientationEventListener.java @@ -21,7 +21,6 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; -import android.util.Config; import android.util.Log; /** @@ -31,7 +30,7 @@ import android.util.Log; public abstract class OrientationEventListener { private static final String TAG = "OrientationEventListener"; private static final boolean DEBUG = false; - private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean localLOGV = false; private int mOrientation = ORIENTATION_UNKNOWN; private SensorManager mSensorManager; private boolean mEnabled = false; diff --git a/core/java/android/view/PointerIcon.aidl b/core/java/android/view/PointerIcon.aidl new file mode 100644 index 0000000..b09340b --- /dev/null +++ b/core/java/android/view/PointerIcon.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +parcelable PointerIcon; diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java new file mode 100644 index 0000000..bb7ed41 --- /dev/null +++ b/core/java/android/view/PointerIcon.java @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import com.android.internal.util.XmlUtils; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** + * Represents an icon that can be used as a mouse pointer. + * <p> + * Pointer icons can be provided either by the system using system styles, + * or by applications using bitmaps or application resources. + * </p> + * + * @hide + */ +public final class PointerIcon implements Parcelable { + private static final String TAG = "PointerIcon"; + + /** Style constant: Custom icon with a user-supplied bitmap. */ + public static final int STYLE_CUSTOM = -1; + + /** Style constant: Null icon. It has no bitmap. */ + public static final int STYLE_NULL = 0; + + /** Style constant: Arrow icon. (Default mouse pointer) */ + public static final int STYLE_ARROW = 1000; + + /** {@hide} Style constant: Spot hover icon for touchpads. */ + public static final int STYLE_SPOT_HOVER = 2000; + + /** {@hide} Style constant: Spot touch icon for touchpads. */ + public static final int STYLE_SPOT_TOUCH = 2001; + + /** {@hide} Style constant: Spot anchor icon for touchpads. */ + public static final int STYLE_SPOT_ANCHOR = 2002; + + // OEM private styles should be defined starting at this range to avoid + // conflicts with any system styles that may be defined in the future. + private static final int STYLE_OEM_FIRST = 10000; + + // The default pointer icon. + private static final int STYLE_DEFAULT = STYLE_ARROW; + + private static final PointerIcon gNullIcon = new PointerIcon(STYLE_NULL); + + private final int mStyle; + private int mSystemIconResourceId; + private Bitmap mBitmap; + private float mHotSpotX; + private float mHotSpotY; + + private PointerIcon(int style) { + mStyle = style; + } + + /** + * Gets a special pointer icon that has no bitmap. + * + * @return The null pointer icon. + * + * @see #STYLE_NULL + */ + public static PointerIcon getNullIcon() { + return gNullIcon; + } + + /** + * Gets the default pointer icon. + * + * @param context The context. + * @return The default pointer icon. + * + * @throws IllegalArgumentException if context is null. + */ + public static PointerIcon getDefaultIcon(Context context) { + return getSystemIcon(context, STYLE_DEFAULT); + } + + /** + * Gets a system pointer icon for the given style. + * If style is not recognized, returns the default pointer icon. + * + * @param context The context. + * @param style The pointer icon style. + * @return The pointer icon. + * + * @throws IllegalArgumentException if context is null. + */ + public static PointerIcon getSystemIcon(Context context, int style) { + if (context == null) { + throw new IllegalArgumentException("context must not be null"); + } + + if (style == STYLE_NULL) { + return gNullIcon; + } + + int styleIndex = getSystemIconStyleIndex(style); + if (styleIndex == 0) { + styleIndex = getSystemIconStyleIndex(STYLE_DEFAULT); + } + + TypedArray a = context.obtainStyledAttributes(null, + com.android.internal.R.styleable.Pointer, + com.android.internal.R.attr.pointerStyle, 0); + int resourceId = a.getResourceId(styleIndex, -1); + a.recycle(); + + if (resourceId == -1) { + Log.w(TAG, "Missing theme resources for pointer icon style " + style); + return style == STYLE_DEFAULT ? gNullIcon : getSystemIcon(context, STYLE_DEFAULT); + } + + PointerIcon icon = new PointerIcon(style); + if ((resourceId & 0xff000000) == 0x01000000) { + icon.mSystemIconResourceId = resourceId; + } else { + icon.loadResource(context.getResources(), resourceId); + } + return icon; + } + + /** + * Creates a custom pointer from the given bitmap and hotspot information. + * + * @param bitmap The bitmap for the icon. + * @param hotspotX The X offset of the pointer icon hotspot in the bitmap. + * Must be within the [0, bitmap.getWidth()) range. + * @param hotspotY The Y offset of the pointer icon hotspot in the bitmap. + * Must be within the [0, bitmap.getHeight()) range. + * @return A pointer icon for this bitmap. + * + * @throws IllegalArgumentException if bitmap is null, or if the x/y hotspot + * parameters are invalid. + */ + public static PointerIcon createCustomIcon(Bitmap bitmap, float hotSpotX, float hotSpotY) { + if (bitmap == null) { + throw new IllegalArgumentException("bitmap must not be null"); + } + validateHotSpot(bitmap, hotSpotX, hotSpotY); + + PointerIcon icon = new PointerIcon(STYLE_CUSTOM); + icon.mBitmap = bitmap; + icon.mHotSpotX = hotSpotX; + icon.mHotSpotY = hotSpotY; + return icon; + } + + /** + * Loads a custom pointer icon from an XML resource. + * <p> + * The XML resource should have the following form: + * <code> + * <?xml version="1.0" encoding="utf-8"?> + * <pointer-icon xmlns:android="http://schemas.android.com/apk/res/android" + * android:bitmap="@drawable/my_pointer_bitmap" + * android:hotSpotX="24" + * android:hotSpotY="24" /> + * </code> + * </p> + * + * @param resources The resources object. + * @param resourceId The resource id. + * @return The pointer icon. + * + * @throws IllegalArgumentException if resources is null. + * @throws Resources.NotFoundException if the resource was not found or the drawable + * linked in the resource was not found. + */ + public static PointerIcon loadCustomIcon(Resources resources, int resourceId) { + if (resources == null) { + throw new IllegalArgumentException("resources must not be null"); + } + + PointerIcon icon = new PointerIcon(STYLE_CUSTOM); + icon.loadResource(resources, resourceId); + return icon; + } + + /** + * Loads the bitmap and hotspot information for a pointer icon, if it is not already loaded. + * Returns a pointer icon (not necessarily the same instance) with the information filled in. + * + * @param context The context. + * @return The loaded pointer icon. + * + * @throws IllegalArgumentException if context is null. + * @see #isLoaded() + * @hide + */ + public PointerIcon load(Context context) { + if (context == null) { + throw new IllegalArgumentException("context must not be null"); + } + + if (mSystemIconResourceId == 0 || mBitmap != null) { + return this; + } + + PointerIcon result = new PointerIcon(mStyle); + result.mSystemIconResourceId = mSystemIconResourceId; + result.loadResource(context.getResources(), mSystemIconResourceId); + return result; + } + + /** + * Returns true if the pointer icon style is {@link #STYLE_NULL}. + * + * @return True if the pointer icon style is {@link #STYLE_NULL}. + */ + public boolean isNullIcon() { + return mStyle == STYLE_NULL; + } + + /** + * Returns true if the pointer icon has been loaded and its bitmap and hotspot + * information are available. + * + * @return True if the pointer icon is loaded. + * @see #load(Context) + */ + public boolean isLoaded() { + return mBitmap != null || mStyle == STYLE_NULL; + } + + /** + * Gets the style of the pointer icon. + * + * @return The pointer icon style. + */ + public int getStyle() { + return mStyle; + } + + /** + * Gets the bitmap of the pointer icon. + * + * @return The pointer icon bitmap, or null if the style is {@link #STYLE_NULL}. + * + * @throws IllegalStateException if the bitmap is not loaded. + * @see #isLoaded() + * @see #load(Context) + */ + public Bitmap getBitmap() { + throwIfIconIsNotLoaded(); + return mBitmap; + } + + /** + * Gets the X offset of the pointer icon hotspot. + * + * @return The hotspot X offset. + * + * @throws IllegalStateException if the bitmap is not loaded. + * @see #isLoaded() + * @see #load(Context) + */ + public float getHotSpotX() { + throwIfIconIsNotLoaded(); + return mHotSpotX; + } + + /** + * Gets the Y offset of the pointer icon hotspot. + * + * @return The hotspot Y offset. + * + * @throws IllegalStateException if the bitmap is not loaded. + * @see #isLoaded() + * @see #load(Context) + */ + public float getHotSpotY() { + throwIfIconIsNotLoaded(); + return mHotSpotY; + } + + private void throwIfIconIsNotLoaded() { + if (!isLoaded()) { + throw new IllegalStateException("The icon is not loaded."); + } + } + + public static final Parcelable.Creator<PointerIcon> CREATOR + = new Parcelable.Creator<PointerIcon>() { + public PointerIcon createFromParcel(Parcel in) { + int style = in.readInt(); + if (style == STYLE_NULL) { + return getNullIcon(); + } + + int systemIconResourceId = in.readInt(); + if (systemIconResourceId != 0) { + PointerIcon icon = new PointerIcon(style); + icon.mSystemIconResourceId = systemIconResourceId; + return icon; + } + + Bitmap bitmap = Bitmap.CREATOR.createFromParcel(in); + float hotSpotX = in.readFloat(); + float hotSpotY = in.readFloat(); + return PointerIcon.createCustomIcon(bitmap, hotSpotX, hotSpotY); + } + + public PointerIcon[] newArray(int size) { + return new PointerIcon[size]; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mStyle); + + if (mStyle != STYLE_NULL) { + out.writeInt(mSystemIconResourceId); + if (mSystemIconResourceId == 0) { + mBitmap.writeToParcel(out, flags); + out.writeFloat(mHotSpotX); + out.writeFloat(mHotSpotY); + } + } + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (other == null || !(other instanceof PointerIcon)) { + return false; + } + + PointerIcon otherIcon = (PointerIcon) other; + if (mStyle != otherIcon.mStyle + || mSystemIconResourceId != otherIcon.mSystemIconResourceId) { + return false; + } + + if (mSystemIconResourceId == 0 && (mBitmap != otherIcon.mBitmap + || mHotSpotX != otherIcon.mHotSpotX + || mHotSpotY != otherIcon.mHotSpotY)) { + return false; + } + + return true; + } + + private void loadResource(Resources resources, int resourceId) { + XmlResourceParser parser = resources.getXml(resourceId); + final int bitmapRes; + final float hotSpotX; + final float hotSpotY; + try { + XmlUtils.beginDocument(parser, "pointer-icon"); + + TypedArray a = resources.obtainAttributes( + parser, com.android.internal.R.styleable.PointerIcon); + bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0); + hotSpotX = a.getFloat(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0); + hotSpotY = a.getFloat(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0); + a.recycle(); + } catch (Exception ex) { + throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex); + } finally { + parser.close(); + } + + if (bitmapRes == 0) { + throw new IllegalArgumentException("<pointer-icon> is missing bitmap attribute."); + } + + Drawable drawable = resources.getDrawable(bitmapRes); + if (!(drawable instanceof BitmapDrawable)) { + throw new IllegalArgumentException("<pointer-icon> bitmap attribute must " + + "refer to a bitmap drawable."); + } + + // Set the properties now that we have successfully loaded the icon. + mBitmap = ((BitmapDrawable)drawable).getBitmap(); + mHotSpotX = hotSpotX; + mHotSpotY = hotSpotY; + } + + private static void validateHotSpot(Bitmap bitmap, float hotSpotX, float hotSpotY) { + if (hotSpotX < 0 || hotSpotX >= bitmap.getWidth()) { + throw new IllegalArgumentException("x hotspot lies outside of the bitmap area"); + } + if (hotSpotY < 0 || hotSpotY >= bitmap.getHeight()) { + throw new IllegalArgumentException("y hotspot lies outside of the bitmap area"); + } + } + + private static int getSystemIconStyleIndex(int style) { + switch (style) { + case STYLE_ARROW: + return com.android.internal.R.styleable.Pointer_pointerIconArrow; + case STYLE_SPOT_HOVER: + return com.android.internal.R.styleable.Pointer_pointerIconSpotHover; + case STYLE_SPOT_TOUCH: + return com.android.internal.R.styleable.Pointer_pointerIconSpotTouch; + case STYLE_SPOT_ANCHOR: + return com.android.internal.R.styleable.Pointer_pointerIconSpotAnchor; + default: + return 0; + } + } +} diff --git a/core/java/android/view/ScaleGestureDetector.java b/core/java/android/view/ScaleGestureDetector.java index d638e70..5e07e1a 100644 --- a/core/java/android/view/ScaleGestureDetector.java +++ b/core/java/android/view/ScaleGestureDetector.java @@ -163,6 +163,13 @@ public class ScaleGestureDetector { private int mActiveId1; private boolean mActive0MostRecent; + /** + * Consistency verifier for debugging purposes. + */ + private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = + InputEventConsistencyVerifier.isInstrumentationEnabled() ? + new InputEventConsistencyVerifier(this, 0) : null; + public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { ViewConfiguration config = ViewConfiguration.get(context); mContext = context; @@ -171,16 +178,20 @@ public class ScaleGestureDetector { } public boolean onTouchEvent(MotionEvent event) { + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTouchEvent(event, 0); + } + final int action = event.getActionMasked(); - boolean handled = true; if (action == MotionEvent.ACTION_DOWN) { reset(); // Start fresh } - if (mInvalidGesture) return false; - - if (!mGestureInProgress) { + boolean handled = true; + if (mInvalidGesture) { + handled = false; + } else if (!mGestureInProgress) { switch (action) { case MotionEvent.ACTION_DOWN: { mActiveId0 = event.getPointerId(0); @@ -456,6 +467,10 @@ public class ScaleGestureDetector { break; } } + + if (!handled && mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); + } return handled; } diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 87b3d79..a98c669 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -34,7 +34,6 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.ParcelFileDescriptor; import android.util.AttributeSet; -import android.util.Config; import android.util.Log; import java.lang.ref.WeakReference; @@ -84,7 +83,7 @@ import java.util.concurrent.locks.ReentrantLock; public class SurfaceView extends View { static private final String TAG = "SurfaceView"; static private final boolean DEBUG = false; - static private final boolean localLOGV = DEBUG ? true : Config.LOGV; + static private final boolean localLOGV = DEBUG ? true : false; final ArrayList<SurfaceHolder.Callback> mCallbacks = new ArrayList<SurfaceHolder.Callback>(); @@ -428,7 +427,7 @@ public class SurfaceView extends View { if (!mHaveFrame) { return; } - ViewRoot viewRoot = (ViewRoot) getRootView().getParent(); + ViewAncestor viewRoot = (ViewAncestor) getRootView().getParent(); if (viewRoot != null) { mTranslator = viewRoot.mTranslator; } diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java new file mode 100644 index 0000000..755ecf5 --- /dev/null +++ b/core/java/android/view/TextureView.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.SurfaceTexture; +import android.util.AttributeSet; +import android.util.Log; + +/** + * <p>A TextureView can be used to display a content stream. Such a content + * stream can for instance be a video or an OpenGL scene. The content stream + * can come from the application's process as well as a remote process.</p> + * + * <p>TextureView can only be used in a hardware accelerated window. When + * rendered in software, TextureView will draw nothing.</p> + * + * <p>Unlike {@link SurfaceView}, TextureView does not create a separate + * window but behaves as a regular View. This key difference allows a + * TextureView to be moved, transformed, animated, etc. For instance, you + * can make a TextureView semi-translucent by calling + * <code>myView.setAlpha(0.5f)</code>.</p> + * + * <p>Using a TextureView is simple: all you need to do is get its + * {@link SurfaceTexture}. The {@link SurfaceTexture} can then be used to + * render content. The following example demonstrates how to render the + * camera preview into a TextureView:</p> + * + * <pre> + * public class LiveCameraActivity extends Activity implements TextureView.SurfaceTextureListener { + * private Camera mCamera; + * private TextureView mTextureView; + * + * protected void onCreate(Bundle savedInstanceState) { + * super.onCreate(savedInstanceState); + * + * mTextureView = new TextureView(this); + * mTextureView.setSurfaceTextureListener(this); + * + * setContentView(mTextureView); + * } + * + * protected void onDestroy() { + * super.onDestroy(); + * + * mCamera.stopPreview(); + * mCamera.release(); + * } + * + * public void onSurfaceTextureAvailable(SurfaceTexture surface) { + * mCamera = Camera.open(); + * + * try { + * mCamera.setPreviewTexture(surface); + * mCamera.startPreview(); + * } catch (IOException ioe) { + * // Something bad happened + * } + * } + * + * public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + * // Ignored, Camera does all the work for us + * } + * } + * </pre> + * + * <p>A TextureView's SurfaceTexture can be obtained either by invoking + * {@link #getSurfaceTexture()} or by using a {@link SurfaceTextureListener}. + * It is important to know that a SurfaceTexture is available only after the + * TextureView is attached to a window (and {@link #onAttachedToWindow()} has + * been invoked.) It is therefore highly recommended you use a listener to + * be notified when the SurfaceTexture becomes available.</p> + * + * @see SurfaceView + * @see SurfaceTexture + */ +public class TextureView extends View { + private HardwareLayer mLayer; + private SurfaceTexture mSurface; + private SurfaceTextureListener mListener; + + private final Runnable mUpdateLayerAction = new Runnable() { + @Override + public void run() { + updateLayer(); + } + }; + private SurfaceTexture.OnFrameAvailableListener mUpdateListener; + + /** + * Creates a new TextureView. + * + * @param context The context to associate this view with. + */ + public TextureView(Context context) { + super(context); + init(); + } + + /** + * Creates a new TextureView. + * + * @param context The context to associate this view with. + * @param attrs The attributes of the XML tag that is inflating the view. + */ + @SuppressWarnings({"UnusedDeclaration"}) + public TextureView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + /** + * Creates a new TextureView. + * + * @param context The context to associate this view with. + * @param attrs The attributes of the XML tag that is inflating the view. + * @param defStyle The default style to apply to this view. If 0, no style + * will be applied (beyond what is included in the theme). This may + * either be an attribute resource, whose value will be retrieved + * from the current theme, or an explicit style resource. + */ + @SuppressWarnings({"UnusedDeclaration"}) + public TextureView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + mLayerPaint = new Paint(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (!isHardwareAccelerated()) { + Log.w("TextureView", "A TextureView or a subclass can only be " + + "used with hardware acceleration enabled."); + } + } + + /** + * The layer type of a TextureView is ignored since a TextureView is always + * considered to act as a hardware layer. The optional paint supplied to this + * method will however be taken into account when rendering the content of + * this TextureView. + * + * @param layerType The ype of layer to use with this view, must be one of + * {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or + * {@link #LAYER_TYPE_HARDWARE} + * @param paint The paint used to compose the layer. This argument is optional + * and can be null. It is ignored when the layer type is + * {@link #LAYER_TYPE_NONE} + */ + @Override + public void setLayerType(int layerType, Paint paint) { + if (paint != mLayerPaint) { + mLayerPaint = paint; + invalidate(); + } + } + + /** + * Always returns {@link #LAYER_TYPE_HARDWARE}. + */ + @Override + public int getLayerType() { + return LAYER_TYPE_HARDWARE; + } + + /** + * Calling this method has no effect. + */ + @Override + public void buildLayer() { + } + + /** + * Subclasses of TextureView cannot do their own rendering + * with the {@link Canvas} object. + * + * @param canvas The Canvas to which the View is rendered. + */ + @Override + public final void draw(Canvas canvas) { + super.draw(canvas); + } + + /** + * Subclasses of TextureView cannot do their own rendering + * with the {@link Canvas} object. + * + * @param canvas The Canvas to which the View is rendered. + */ + @Override + protected final void onDraw(Canvas canvas) { + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (mSurface != null) { + nSetDefaultBufferSize(mSurface.mSurfaceTexture, getWidth(), getHeight()); + } + } + + @Override + HardwareLayer getHardwareLayer() { + if (mAttachInfo == null || mAttachInfo.mHardwareRenderer == null) { + return null; + } + + if (mLayer == null) { + mLayer = mAttachInfo.mHardwareRenderer.createHardwareLayer(); + mSurface = mAttachInfo.mHardwareRenderer.createSuraceTexture(mLayer); + nSetDefaultBufferSize(mSurface.mSurfaceTexture, getWidth(), getHeight()); + + mUpdateListener = new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + // Per SurfaceTexture's documentation, the callback may be invoked + // from an arbitrary thread + post(mUpdateLayerAction); + } + }; + mSurface.setOnFrameAvailableListener(mUpdateListener); + + if (mListener != null) { + mListener.onSurfaceTextureAvailable(mSurface); + } + } + + return mLayer; + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + + if (mSurface != null) { + // When the view becomes invisible, stop updating it, it's a waste of CPU + // To cancel updates, the easiest thing to do is simply to remove the + // updates listener + if (visibility == VISIBLE) { + mSurface.setOnFrameAvailableListener(mUpdateListener); + updateLayer(); + } else { + mSurface.setOnFrameAvailableListener(null); + } + } + } + + private void updateLayer() { + if (mAttachInfo == null || mAttachInfo.mHardwareRenderer == null) { + return; + } + + mAttachInfo.mHardwareRenderer.updateTextureLayer(mLayer, getWidth(), getHeight(), mSurface); + + invalidate(); + } + + /** + * Returns the {@link SurfaceTexture} used by this view. This method + * may return null if the view is not attached to a window. + */ + public SurfaceTexture getSurfaceTexture() { + return mSurface; + } + + /** + * Returns the {@link SurfaceTextureListener} currently associated with this + * texture view. + * + * @see #setSurfaceTextureListener(android.view.TextureView.SurfaceTextureListener) + * @see SurfaceTextureListener + */ + public SurfaceTextureListener getSurfaceTextureListener() { + return mListener; + } + + /** + * Sets the {@link SurfaceTextureListener} used to listen to surface + * texture events. + * + * @see #getSurfaceTextureListener() + * @see SurfaceTextureListener + */ + public void setSurfaceTextureListener(SurfaceTextureListener listener) { + mListener = listener; + } + + /** + * This listener can be used to be notified when the surface texture + * associated with this texture view is available. + */ + public static interface SurfaceTextureListener { + /** + * Invoked when a {@link TextureView}'s SurfaceTexture is ready for use. + * + * @param surface The surface returned by + * {@link android.view.TextureView#getSurfaceTexture()} + */ + public void onSurfaceTextureAvailable(SurfaceTexture surface); + + /** + * Invoked when the {@link SurfaceTexture}'s buffers size changed. + * + * @param surface The surface returned by + * {@link android.view.TextureView#getSurfaceTexture()} + * @param width The new width of the surface + * @param height The new height of the surface + */ + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height); + } + + private static native void nSetDefaultBufferSize(int surfaceTexture, int width, int height); +} diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java index 4ab2881..fccef2b 100644 --- a/core/java/android/view/VelocityTracker.java +++ b/core/java/android/view/VelocityTracker.java @@ -16,8 +16,6 @@ package android.view; -import android.util.Config; -import android.util.Log; import android.util.Poolable; import android.util.Pool; import android.util.Pools; @@ -25,24 +23,15 @@ import android.util.PoolableManager; /** * Helper for tracking the velocity of touch events, for implementing - * flinging and other such gestures. Use {@link #obtain} to retrieve a - * new instance of the class when you are going to begin tracking, put - * the motion events you receive into it with {@link #addMovement(MotionEvent)}, - * and when you want to determine the velocity call - * {@link #computeCurrentVelocity(int)} and then {@link #getXVelocity()} - * and {@link #getXVelocity()}. + * flinging and other such gestures. + * + * Use {@link #obtain} to retrieve a new instance of the class when you are going + * to begin tracking. Put the motion events you receive into it with + * {@link #addMovement(MotionEvent)}. When you want to determine the velocity call + * {@link #computeCurrentVelocity(int)} and then call {@link #getXVelocity(int)} + * and {@link #getXVelocity(int)} to retrieve the velocity for each pointer id. */ public final class VelocityTracker implements Poolable<VelocityTracker> { - private static final String TAG = "VelocityTracker"; - private static final boolean DEBUG = false; - private static final boolean localLOGV = DEBUG || Config.LOGV; - - private static final int NUM_PAST = 10; - private static final int MAX_AGE_MILLISECONDS = 200; - - private static final int POINTER_POOL_CAPACITY = 20; - private static final int INVALID_POINTER = -1; - private static final Pool<VelocityTracker> sPool = Pools.synchronizedPool( Pools.finitePool(new PoolableManager<VelocityTracker>() { public VelocityTracker newInstance() { @@ -56,31 +45,20 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { element.clear(); } }, 2)); - - private static Pointer sRecycledPointerListHead; - private static int sRecycledPointerCount; - - private static final class Pointer { - public Pointer next; - - public int id; - public float xVelocity; - public float yVelocity; - - public final float[] pastX = new float[NUM_PAST]; - public final float[] pastY = new float[NUM_PAST]; - public final long[] pastTime = new long[NUM_PAST]; // uses Long.MIN_VALUE as a sentinel - - public int generation; - } - - private Pointer mPointerListHead; // sorted by id in increasing order - private int mLastTouchIndex; - private int mGeneration; - private int mActivePointerId; + private static final int ACTIVE_POINTER_ID = -1; + + private int mPtr; private VelocityTracker mNext; + private static native int nativeInitialize(); + private static native void nativeDispose(int ptr); + private static native void nativeClear(int ptr); + private static native void nativeAddMovement(int ptr, MotionEvent event); + private static native void nativeComputeCurrentVelocity(int ptr, int units, float maxVelocity); + private static native float nativeGetXVelocity(int ptr, int id); + private static native float nativeGetYVelocity(int ptr, int id); + /** * Retrieve a new VelocityTracker object to watch the velocity of a * motion. Be sure to call {@link #recycle} when done. You should @@ -116,18 +94,26 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { } private VelocityTracker() { - clear(); + mPtr = nativeInitialize(); } - + + @Override + protected void finalize() throws Throwable { + try { + if (mPtr != 0) { + nativeDispose(mPtr); + mPtr = 0; + } + } finally { + super.finalize(); + } + } + /** * Reset the velocity tracker back to its initial state. */ public void clear() { - releasePointerList(mPointerListHead); - - mPointerListHead = null; - mLastTouchIndex = 0; - mActivePointerId = INVALID_POINTER; + nativeClear(mPtr); } /** @@ -137,110 +123,13 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * final {@link MotionEvent#ACTION_UP}. You can, however, call this * for whichever events you desire. * - * @param ev The MotionEvent you received and would like to track. + * @param event The MotionEvent you received and would like to track. */ - public void addMovement(MotionEvent ev) { - final int historySize = ev.getHistorySize(); - final int pointerCount = ev.getPointerCount(); - final int lastTouchIndex = mLastTouchIndex; - final int nextTouchIndex = (lastTouchIndex + 1) % NUM_PAST; - final int finalTouchIndex = (nextTouchIndex + historySize) % NUM_PAST; - final int generation = mGeneration++; - - mLastTouchIndex = finalTouchIndex; - - // Update pointer data. - Pointer previousPointer = null; - for (int i = 0; i < pointerCount; i++){ - final int pointerId = ev.getPointerId(i); - - // Find the pointer data for this pointer id. - // This loop is optimized for the common case where pointer ids in the event - // are in sorted order. However, we check for this case explicitly and - // perform a full linear scan from the start if needed. - Pointer nextPointer; - if (previousPointer == null || pointerId < previousPointer.id) { - previousPointer = null; - nextPointer = mPointerListHead; - } else { - nextPointer = previousPointer.next; - } - - final Pointer pointer; - for (;;) { - if (nextPointer != null) { - final int nextPointerId = nextPointer.id; - if (nextPointerId == pointerId) { - pointer = nextPointer; - break; - } - if (nextPointerId < pointerId) { - nextPointer = nextPointer.next; - continue; - } - } - - // Pointer went down. Add it to the list. - // Write a sentinel at the end of the pastTime trace so we will be able to - // tell when the trace started. - if (mActivePointerId == INVALID_POINTER) { - // Congratulations! You're the new active pointer! - mActivePointerId = pointerId; - } - pointer = obtainPointer(); - pointer.id = pointerId; - pointer.pastTime[lastTouchIndex] = Long.MIN_VALUE; - pointer.next = nextPointer; - if (previousPointer == null) { - mPointerListHead = pointer; - } else { - previousPointer.next = pointer; - } - break; - } - - pointer.generation = generation; - previousPointer = pointer; - - final float[] pastX = pointer.pastX; - final float[] pastY = pointer.pastY; - final long[] pastTime = pointer.pastTime; - - for (int j = 0; j < historySize; j++) { - final int touchIndex = (nextTouchIndex + j) % NUM_PAST; - pastX[touchIndex] = ev.getHistoricalX(i, j); - pastY[touchIndex] = ev.getHistoricalY(i, j); - pastTime[touchIndex] = ev.getHistoricalEventTime(j); - } - pastX[finalTouchIndex] = ev.getX(i); - pastY[finalTouchIndex] = ev.getY(i); - pastTime[finalTouchIndex] = ev.getEventTime(); - } - - // Find removed pointers. - previousPointer = null; - for (Pointer pointer = mPointerListHead; pointer != null; ) { - final Pointer nextPointer = pointer.next; - final int pointerId = pointer.id; - if (pointer.generation != generation) { - // Pointer went up. Remove it from the list. - if (previousPointer == null) { - mPointerListHead = nextPointer; - } else { - previousPointer.next = nextPointer; - } - releasePointer(pointer); - - if (pointerId == mActivePointerId) { - // Pick a new active pointer. How is arbitrary. - mActivePointerId = mPointerListHead != null ? - mPointerListHead.id : INVALID_POINTER; - } - } else { - previousPointer = pointer; - } - pointer = nextPointer; + public void addMovement(MotionEvent event) { + if (event == null) { + throw new IllegalArgumentException("event must not be null"); } + nativeAddMovement(mPtr, event); } /** @@ -250,7 +139,7 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * @see #computeCurrentVelocity(int, float) */ public void computeCurrentVelocity(int units) { - computeCurrentVelocity(units, Float.MAX_VALUE); + nativeComputeCurrentVelocity(mPtr, units, Float.MAX_VALUE); } /** @@ -267,78 +156,7 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * must be positive. */ public void computeCurrentVelocity(int units, float maxVelocity) { - final int lastTouchIndex = mLastTouchIndex; - - for (Pointer pointer = mPointerListHead; pointer != null; pointer = pointer.next) { - final long[] pastTime = pointer.pastTime; - - // Search backwards in time for oldest acceptable time. - // Stop at the beginning of the trace as indicated by the sentinel time Long.MIN_VALUE. - int oldestTouchIndex = lastTouchIndex; - int numTouches = 1; - final long minTime = pastTime[lastTouchIndex] - MAX_AGE_MILLISECONDS; - while (numTouches < NUM_PAST) { - final int nextOldestTouchIndex = (oldestTouchIndex + NUM_PAST - 1) % NUM_PAST; - final long nextOldestTime = pastTime[nextOldestTouchIndex]; - if (nextOldestTime < minTime) { // also handles end of trace sentinel - break; - } - oldestTouchIndex = nextOldestTouchIndex; - numTouches += 1; - } - - // If we have a lot of samples, skip the last received sample since it is - // probably pretty noisy compared to the sum of all of the traces already acquired. - if (numTouches > 3) { - numTouches -= 1; - } - - // Kind-of stupid. - final float[] pastX = pointer.pastX; - final float[] pastY = pointer.pastY; - - final float oldestX = pastX[oldestTouchIndex]; - final float oldestY = pastY[oldestTouchIndex]; - final long oldestTime = pastTime[oldestTouchIndex]; - - float accumX = 0; - float accumY = 0; - - for (int i = 1; i < numTouches; i++) { - final int touchIndex = (oldestTouchIndex + i) % NUM_PAST; - final int duration = (int)(pastTime[touchIndex] - oldestTime); - - if (duration == 0) continue; - - float delta = pastX[touchIndex] - oldestX; - float velocity = (delta / duration) * units; // pixels/frame. - accumX = (accumX == 0) ? velocity : (accumX + velocity) * .5f; - - delta = pastY[touchIndex] - oldestY; - velocity = (delta / duration) * units; // pixels/frame. - accumY = (accumY == 0) ? velocity : (accumY + velocity) * .5f; - } - - if (accumX < -maxVelocity) { - accumX = - maxVelocity; - } else if (accumX > maxVelocity) { - accumX = maxVelocity; - } - - if (accumY < -maxVelocity) { - accumY = - maxVelocity; - } else if (accumY > maxVelocity) { - accumY = maxVelocity; - } - - pointer.xVelocity = accumX; - pointer.yVelocity = accumY; - - if (localLOGV) { - Log.v(TAG, "Pointer " + pointer.id - + ": Y velocity=" + accumX +" X velocity=" + accumY + " N=" + numTouches); - } - } + nativeComputeCurrentVelocity(mPtr, units, maxVelocity); } /** @@ -348,8 +166,7 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * @return The previously computed X velocity. */ public float getXVelocity() { - Pointer pointer = getPointer(mActivePointerId); - return pointer != null ? pointer.xVelocity : 0; + return nativeGetXVelocity(mPtr, ACTIVE_POINTER_ID); } /** @@ -359,8 +176,7 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * @return The previously computed Y velocity. */ public float getYVelocity() { - Pointer pointer = getPointer(mActivePointerId); - return pointer != null ? pointer.yVelocity : 0; + return nativeGetYVelocity(mPtr, ACTIVE_POINTER_ID); } /** @@ -371,8 +187,7 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * @return The previously computed X velocity. */ public float getXVelocity(int id) { - Pointer pointer = getPointer(id); - return pointer != null ? pointer.xVelocity : 0; + return nativeGetXVelocity(mPtr, id); } /** @@ -383,68 +198,6 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * @return The previously computed Y velocity. */ public float getYVelocity(int id) { - Pointer pointer = getPointer(id); - return pointer != null ? pointer.yVelocity : 0; - } - - private Pointer getPointer(int id) { - for (Pointer pointer = mPointerListHead; pointer != null; pointer = pointer.next) { - if (pointer.id == id) { - return pointer; - } - } - return null; - } - - private static Pointer obtainPointer() { - synchronized (sPool) { - if (sRecycledPointerCount != 0) { - Pointer element = sRecycledPointerListHead; - sRecycledPointerCount -= 1; - sRecycledPointerListHead = element.next; - element.next = null; - return element; - } - } - return new Pointer(); - } - - private static void releasePointer(Pointer pointer) { - synchronized (sPool) { - if (sRecycledPointerCount < POINTER_POOL_CAPACITY) { - pointer.next = sRecycledPointerListHead; - sRecycledPointerCount += 1; - sRecycledPointerListHead = pointer; - } - } - } - - private static void releasePointerList(Pointer pointer) { - if (pointer != null) { - synchronized (sPool) { - int count = sRecycledPointerCount; - if (count >= POINTER_POOL_CAPACITY) { - return; - } - - Pointer tail = pointer; - for (;;) { - count += 1; - if (count >= POINTER_POOL_CAPACITY) { - break; - } - - Pointer next = tail.next; - if (next == null) { - break; - } - tail = next; - } - - tail.next = sRecycledPointerListHead; - sRecycledPointerCount = count; - sRecycledPointerListHead = pointer; - } - } + return nativeGetYVelocity(mPtr, id); } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 8e3e699..d5f573c 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -49,7 +49,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.os.SystemClock; -import android.os.SystemProperties; import android.util.AttributeSet; import android.util.Log; import android.util.Pool; @@ -128,11 +127,11 @@ import java.util.concurrent.CopyOnWriteArrayList; * that will be notified when something interesting happens to the view. For * example, all views will let you set a listener to be notified when the view * gains or loses focus. You can register such a listener using - * {@link #setOnFocusChangeListener}. Other view subclasses offer more - * specialized listeners. For example, a Button exposes a listener to notify - * clients when the button is clicked.</li> + * {@link #setOnFocusChangeListener(android.view.View.OnFocusChangeListener)}. + * Other view subclasses offer more specialized listeners. For example, a Button + * exposes a listener to notify clients when the button is clicked.</li> * <li><strong>Set visibility:</strong> You can hide or show views using - * {@link #setVisibility}.</li> + * {@link #setVisibility(int)}.</li> * </ul> * </p> * <p><em> @@ -173,61 +172,61 @@ import java.util.concurrent.CopyOnWriteArrayList; * * <tr> * <td rowspan="3">Layout</td> - * <td><code>{@link #onMeasure}</code></td> + * <td><code>{@link #onMeasure(int, int)}</code></td> * <td>Called to determine the size requirements for this view and all * of its children. * </td> * </tr> * <tr> - * <td><code>{@link #onLayout}</code></td> + * <td><code>{@link #onLayout(boolean, int, int, int, int)}</code></td> * <td>Called when this view should assign a size and position to all * of its children. * </td> * </tr> * <tr> - * <td><code>{@link #onSizeChanged}</code></td> + * <td><code>{@link #onSizeChanged(int, int, int, int)}</code></td> * <td>Called when the size of this view has changed. * </td> * </tr> * * <tr> * <td>Drawing</td> - * <td><code>{@link #onDraw}</code></td> + * <td><code>{@link #onDraw(android.graphics.Canvas)}</code></td> * <td>Called when the view should render its content. * </td> * </tr> * * <tr> * <td rowspan="4">Event processing</td> - * <td><code>{@link #onKeyDown}</code></td> + * <td><code>{@link #onKeyDown(int, KeyEvent)}</code></td> * <td>Called when a new key event occurs. * </td> * </tr> * <tr> - * <td><code>{@link #onKeyUp}</code></td> + * <td><code>{@link #onKeyUp(int, KeyEvent)}</code></td> * <td>Called when a key up event occurs. * </td> * </tr> * <tr> - * <td><code>{@link #onTrackballEvent}</code></td> + * <td><code>{@link #onTrackballEvent(MotionEvent)}</code></td> * <td>Called when a trackball motion event occurs. * </td> * </tr> * <tr> - * <td><code>{@link #onTouchEvent}</code></td> + * <td><code>{@link #onTouchEvent(MotionEvent)}</code></td> * <td>Called when a touch screen motion event occurs. * </td> * </tr> * * <tr> * <td rowspan="2">Focus</td> - * <td><code>{@link #onFocusChanged}</code></td> + * <td><code>{@link #onFocusChanged(boolean, int, android.graphics.Rect)}</code></td> * <td>Called when the view gains or loses focus. * </td> * </tr> * * <tr> - * <td><code>{@link #onWindowFocusChanged}</code></td> + * <td><code>{@link #onWindowFocusChanged(boolean)}</code></td> * <td>Called when the window containing the view gains or loses focus. * </td> * </tr> @@ -246,7 +245,7 @@ import java.util.concurrent.CopyOnWriteArrayList; * </tr> * * <tr> - * <td><code>{@link #onWindowVisibilityChanged}</code></td> + * <td><code>{@link #onWindowVisibilityChanged(int)}</code></td> * <td>Called when the visibility of the window containing the view * has changed. * </td> @@ -562,15 +561,15 @@ import java.util.concurrent.CopyOnWriteArrayList; * As a remedy, the framework offers a touch filtering mechanism that can be used to * improve the security of views that provide access to sensitive functionality. * </p><p> - * To enable touch filtering, call {@link #setFilterTouchesWhenObscured} or set the + * To enable touch filtering, call {@link #setFilterTouchesWhenObscured(boolean)} or set the * android:filterTouchesWhenObscured layout attribute to true. When enabled, the framework * will discard touches that are received whenever the view's window is obscured by * another visible window. As a result, the view will not receive touches whenever a * toast, dialog or other window appears above the view's window. * </p><p> * For more fine-grained control over security, consider overriding the - * {@link #onFilterTouchEventForSecurity} method to implement your own security policy. - * See also {@link MotionEvent#FLAG_WINDOW_IS_OBSCURED}. + * {@link #onFilterTouchEventForSecurity(MotionEvent)} method to implement your own + * security policy. See also {@link MotionEvent#FLAG_WINDOW_IS_OBSCURED}. * </p> * * @attr ref android.R.styleable#View_alpha @@ -668,19 +667,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility private static final int FITS_SYSTEM_WINDOWS = 0x00000002; /** - * This view is visible. Use with {@link #setVisibility}. + * This view is visible. Use with {@link #setVisibility(int)}. */ public static final int VISIBLE = 0x00000000; /** * This view is invisible, but it still takes up space for layout purposes. - * Use with {@link #setVisibility}. + * Use with {@link #setVisibility(int)}. */ public static final int INVISIBLE = 0x00000004; /** * This view is invisible, and it doesn't take any space for layout - * purposes. Use with {@link #setVisibility}. + * purposes. Use with {@link #setVisibility(int)}. */ public static final int GONE = 0x00000008; @@ -714,10 +713,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility static final int ENABLED_MASK = 0x00000020; /** - * This view won't draw. {@link #onDraw} won't be called and further - * optimizations - * will be performed. It is okay to have this flag set and a background. - * Use with DRAW_MASK when calling setFlags. + * This view won't draw. {@link #onDraw(android.graphics.Canvas)} won't be + * called and further optimizations will be performed. It is okay to have + * this flag set and a background. Use with DRAW_MASK when calling setFlags. * {@hide} */ static final int WILL_NOT_DRAW = 0x00000080; @@ -951,6 +949,43 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility static final int PARENT_SAVE_DISABLED_MASK = 0x20000000; /** + * Horizontal direction of this view is from Left to Right. + * Use with {@link #setHorizontalDirection}. + * {@hide} + */ + public static final int HORIZONTAL_DIRECTION_LTR = 0x00000000; + + /** + * Horizontal direction of this view is from Right to Left. + * Use with {@link #setHorizontalDirection}. + * {@hide} + */ + public static final int HORIZONTAL_DIRECTION_RTL = 0x40000000; + + /** + * Horizontal direction of this view is inherited from its parent. + * Use with {@link #setHorizontalDirection}. + * {@hide} + */ + public static final int HORIZONTAL_DIRECTION_INHERIT = 0x80000000; + + /** + * Horizontal direction of this view is from deduced from the default language + * script for the locale. Use with {@link #setHorizontalDirection}. + * {@hide} + */ + public static final int HORIZONTAL_DIRECTION_LOCALE = 0xC0000000; + + /** + * Mask for use with setFlags indicating bits used for horizontalDirection. + * {@hide} + */ + static final int HORIZONTAL_DIRECTION_MASK = 0xC0000000; + + private static final int[] HORIZONTAL_DIRECTION_FLAGS = { HORIZONTAL_DIRECTION_LTR, + HORIZONTAL_DIRECTION_RTL, HORIZONTAL_DIRECTION_INHERIT, HORIZONTAL_DIRECTION_LOCALE}; + + /** * View flag indicating whether {@link #addFocusables(ArrayList, int, int)} * should add all focusable Views regardless if they are focusable in touch mode. */ @@ -963,34 +998,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility public static final int FOCUSABLES_TOUCH_MODE = 0x00000001; /** - * Use with {@link #focusSearch}. Move focus to the previous selectable + * Use with {@link #focusSearch(int)}. Move focus to the previous selectable * item. */ public static final int FOCUS_BACKWARD = 0x00000001; /** - * Use with {@link #focusSearch}. Move focus to the next selectable + * Use with {@link #focusSearch(int)}. Move focus to the next selectable * item. */ public static final int FOCUS_FORWARD = 0x00000002; /** - * Use with {@link #focusSearch}. Move focus to the left. + * Use with {@link #focusSearch(int)}. Move focus to the left. */ public static final int FOCUS_LEFT = 0x00000011; /** - * Use with {@link #focusSearch}. Move focus up. + * Use with {@link #focusSearch(int)}. Move focus up. */ public static final int FOCUS_UP = 0x00000021; /** - * Use with {@link #focusSearch}. Move focus to the right. + * Use with {@link #focusSearch(int)}. Move focus to the right. */ public static final int FOCUS_RIGHT = 0x00000042; /** - * Use with {@link #focusSearch}. Move focus down. + * Use with {@link #focusSearch(int)}. Move focus down. */ public static final int FOCUS_DOWN = 0x00000082; @@ -1304,6 +1339,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility static final int VIEW_STATE_PRESSED = 1 << 4; static final int VIEW_STATE_ACTIVATED = 1 << 5; static final int VIEW_STATE_ACCELERATED = 1 << 6; + static final int VIEW_STATE_HOVERED = 1 << 7; + static final int VIEW_STATE_DRAG_CAN_ACCEPT = 1 << 8; + static final int VIEW_STATE_DRAG_HOVERED = 1 << 9; static final int[] VIEW_STATE_IDS = new int[] { R.attr.state_window_focused, VIEW_STATE_WINDOW_FOCUSED, @@ -1313,6 +1351,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility R.attr.state_pressed, VIEW_STATE_PRESSED, R.attr.state_activated, VIEW_STATE_ACTIVATED, R.attr.state_accelerated, VIEW_STATE_ACCELERATED, + R.attr.state_hovered, VIEW_STATE_HOVERED, + R.attr.state_drag_can_accept, VIEW_STATE_DRAG_CAN_ACCEPT, + R.attr.state_drag_hovered, VIEW_STATE_DRAG_HOVERED, }; static { @@ -1623,6 +1664,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility private static final int AWAKEN_SCROLL_BARS_ON_ATTACH = 0x08000000; /** + * Indicates that the view has received HOVER_ENTER. Cleared on HOVER_EXIT. + * @hide + */ + private static final int HOVERED = 0x10000000; + + /** * Indicates that pivotX or pivotY were explicitly set and we should not assume the center * for transform operations * @@ -1643,6 +1690,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ static final int INVALIDATED = 0x80000000; + /* Masks for mPrivateFlags2 */ + + /** + * Indicates that this view has reported that it can accept the current drag's content. + * Cleared when the drag operation concludes. + * @hide + */ + static final int DRAG_CAN_ACCEPT = 0x00000001; + + /** + * Indicates that this view is currently directly under the drag location in a + * drag-and-drop operation involving content that it can accept. Cleared when + * the drag exits the view, or when the drag operation concludes. + * @hide + */ + static final int DRAG_HOVERED = 0x00000002; + + /** + * Indicates whether the view is drawn in right-to-left direction. + * + * @hide + */ + static final int RESOLVED_LAYOUT_RTL = 0x00000004; + + /* End of masks for mPrivateFlags2 */ + + static final int DRAG_MASK = DRAG_CAN_ACCEPT | DRAG_HOVERED; + /** * Always allow a user to over-scroll this view, provided it is a * view that can scroll. @@ -1814,6 +1889,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility @ViewDebug.FlagToString(mask = DIRTY_MASK, equals = DIRTY, name = "DIRTY") }) int mPrivateFlags; + int mPrivateFlags2; /** * This view's request for the visibility of the status bar. @@ -2243,12 +2319,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility private ViewPropertyAnimator mAnimator = null; /** - * Cache drag/drop state - * - */ - boolean mCanAcceptDrop; - - /** * Flag indicating that a drag can cross window boundaries. When * {@link #startDrag(ClipData, DragShadowBuilder, Object, int)} is called * with this flag set, all visible applications will be able to participate @@ -2356,6 +2426,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility Rect mLocalDirtyRect; /** + * Consistency verifier for debugging purposes. + * @hide + */ + protected final InputEventConsistencyVerifier mInputEventConsistencyVerifier = + InputEventConsistencyVerifier.isInstrumentationEnabled() ? + new InputEventConsistencyVerifier(this, 0) : null; + + /** * Simple constructor to use when creating a view from code. * * @param context The Context the view is running in, through which it can @@ -2562,6 +2640,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility viewFlagMasks |= VISIBILITY_MASK; } break; + case com.android.internal.R.styleable.View_horizontalDirection: + final int layoutDirection = a.getInt(attr, 0); + if (layoutDirection != 0) { + viewFlagValues |= HORIZONTAL_DIRECTION_FLAGS[layoutDirection]; + viewFlagMasks |= HORIZONTAL_DIRECTION_MASK; + } + break; case com.android.internal.R.styleable.View_drawingCacheQuality: final int cacheQuality = a.getInt(attr, 0); if (cacheQuality != 0) { @@ -2799,8 +2884,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Set the size of the faded edge used to indicate that more content in this * view is available. Will not change whether the fading edge is enabled; use - * {@link #setVerticalFadingEdgeEnabled} or {@link #setHorizontalFadingEdgeEnabled} - * to enable the fading edge for the vertical or horizontal fading edges. + * {@link #setVerticalFadingEdgeEnabled(boolean)} or + * {@link #setHorizontalFadingEdgeEnabled(boolean)} to enable the fading edge + * for the vertical or horizontal fading edges. * * @param length The size in pixels of the faded edge used to indicate that more * content in this view is visible. @@ -3137,6 +3223,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Performs button-related actions during a touch down event. + * + * @param event The event. + * @return True if the down was consumed. + * + * @hide + */ + protected boolean performButtonActionOnTouchDown(MotionEvent event) { + if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) { + if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) { + return true; + } + } + return false; + } + + /** * Bring up the context menu for this view. * * @return Whether a context menu was displayed. @@ -3146,6 +3249,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Bring up the context menu for this view, referring to the item under the specified point. + * + * @param x The referenced x coordinate. + * @param y The referenced y coordinate. + * @param metaState The keyboard modifiers that were pressed. + * @return Whether a context menu was displayed. + * + * @hide + */ + public boolean showContextMenu(float x, float y, int metaState) { + return showContextMenu(); + } + + /** * Start an action mode. * * @param callback Callback that will control the lifecycle of the action mode @@ -3193,7 +3310,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * Give this view focus. This will cause {@link #onFocusChanged} to be called. + * Give this view focus. This will cause + * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called. * * Note: this does not check whether this {@link View} should get focus, it just * gives it focus no matter what. It should only be called internally by framework @@ -3278,7 +3396,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Called when this view wants to give up focus. This will cause - * {@link #onFocusChanged} to be called. + * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called. */ public void clearFocus() { if (DBG) { @@ -3404,7 +3522,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * {@inheritDoc} + * Sends an accessibility event of the given type. If accessiiblity is + * not enabled this method has no effect. The default implementation calls + * {@link #onInitializeAccessibilityEvent(AccessibilityEvent)} first + * to populate information about the event source (this View), then calls + * {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)} to + * populate the text content of the event source including its descendants, + * and last calls + * {@link ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} + * on its parent to resuest sending of the event to interested parties. + * + * @param eventType The type of the event to send. + * + * @see #onInitializeAccessibilityEvent(AccessibilityEvent) + * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent) + * @see ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent) */ public void sendAccessibilityEvent(int eventType) { if (AccessibilityManager.getInstance(mContext).isEnabled()) { @@ -3413,12 +3545,93 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * {@inheritDoc} + * This method behaves exactly as {@link #sendAccessibilityEvent(int)} but + * takes as an argument an empty {@link AccessibilityEvent} and does not + * perfrom a check whether accessibility is enabled. + * + * @param event The event to send. + * + * @see #sendAccessibilityEvent(int) */ public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { if (!isShown()) { return; } + onInitializeAccessibilityEvent(event); + dispatchPopulateAccessibilityEvent(event); + // In the beginning we called #isShown(), so we know that getParent() is not null. + getParent().requestSendAccessibilityEvent(this, event); + } + + /** + * Dispatches an {@link AccessibilityEvent} to the {@link View} first and then + * to its children for adding their text content to the event. Note that the + * event text is populated in a separate dispatch path since we add to the + * event not only the text of the source but also the text of all its descendants. + * </p> + * A typical implementation will call + * {@link #onPopulateAccessibilityEvent(AccessibilityEvent)} on the this view + * and then call the {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)} + * on each child. Override this method if custom population of the event text + * content is required. + * + * @param event The event. + * + * @return True if the event population was completed. + */ + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); + return false; + } + + /** + * Called from {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)} + * giving a chance to this View to populate the accessibility event with its + * text content. While the implementation is free to modify other event + * attributes this should be performed in + * {@link #onInitializeAccessibilityEvent(AccessibilityEvent)}. + * <p> + * Example: Adding formatted date string to an accessibility event in addition + * to the text added by the super implementation. + * </p><p><pre><code> + * public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + * super.onPopulateAccessibilityEvent(event); + * final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY; + * String selectedDateUtterance = DateUtils.formatDateTime(mContext, + * mCurrentDate.getTimeInMillis(), flags); + * event.getText().add(selectedDateUtterance); + * } + * </code></pre></p> + * + * @param event The accessibility event which to populate. + * + * @see #sendAccessibilityEvent(int) + * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent) + */ + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + + } + + /** + * Initializes an {@link AccessibilityEvent} with information about the + * the type of the event and this View which is the event source. In other + * words, the source of an accessibility event is the view whose state + * change triggered firing the event. + * <p> + * Example: Setting the password property of an event in addition + * to properties set by the super implementation. + * </p><p><pre><code> + * public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + * super.onInitializeAccessibilityEvent(event); + * event.setPassword(true); + * } + * </code></pre></p> + * @param event The event to initialeze. + * + * @see #sendAccessibilityEvent(int) + * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent) + */ + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { event.setClassName(getClass().getName()); event.setPackageName(getContext().getPackageName()); event.setEnabled(isEnabled()); @@ -3431,22 +3644,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility event.setCurrentItemIndex(focusablesTempList.indexOf(this)); focusablesTempList.clear(); } - - dispatchPopulateAccessibilityEvent(event); - - AccessibilityManager.getInstance(mContext).sendAccessibilityEvent(event); - } - - /** - * Dispatches an {@link AccessibilityEvent} to the {@link View} children - * to be populated. - * - * @param event The event. - * - * @return True if the event population was completed. - */ - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - return false; } /** @@ -3924,11 +4121,47 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Returns the horizontal direction for this view. + * + * @return One of {@link #HORIZONTAL_DIRECTION_LTR}, + * {@link #HORIZONTAL_DIRECTION_RTL}, + * {@link #HORIZONTAL_DIRECTION_INHERIT} or + * {@link #HORIZONTAL_DIRECTION_LOCALE}. + * @attr ref android.R.styleable#View_horizontalDirection + * @hide + */ + @ViewDebug.ExportedProperty(mapping = { + @ViewDebug.IntToString(from = HORIZONTAL_DIRECTION_LTR, to = "LTR"), + @ViewDebug.IntToString(from = HORIZONTAL_DIRECTION_RTL, to = "RTL"), + @ViewDebug.IntToString(from = HORIZONTAL_DIRECTION_INHERIT, to = "INHERIT"), + @ViewDebug.IntToString(from = HORIZONTAL_DIRECTION_LOCALE, to = "LOCALE") + }) + public int getHorizontalDirection() { + return mViewFlags & HORIZONTAL_DIRECTION_MASK; + } + + /** + * Set the horizontal direction for this view. + * + * @param horizontalDirection One of {@link #HORIZONTAL_DIRECTION_LTR}, + * {@link #HORIZONTAL_DIRECTION_RTL}, + * {@link #HORIZONTAL_DIRECTION_INHERIT} or + * {@link #HORIZONTAL_DIRECTION_LOCALE}. + * @attr ref android.R.styleable#View_horizontalDirection + * @hide + */ + @RemotableViewMethod + public void setHorizontalDirection(int horizontalDirection) { + setFlags(horizontalDirection, HORIZONTAL_DIRECTION_MASK); + } + + /** * If this view doesn't do any drawing on its own, set this flag to * allow further optimizations. By default, this flag is not set on * View, but could be set on some View subclasses such as ViewGroup. * - * Typically, if you override {@link #onDraw} you should clear this flag. + * Typically, if you override {@link #onDraw(android.graphics.Canvas)} + * you should clear this flag. * * @param willNotDraw whether or not this View draw on its own */ @@ -4057,7 +4290,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * {@link #setPressed(boolean)} is explicitly called, only clickable views can enter * the pressed state. * - * @see #setPressed + * @see #setPressed(boolean) * @see #isClickable() * @see #setClickable(boolean) * @@ -4084,7 +4317,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * Controls whether the saving of this view's state is * enabled (that is, whether its {@link #onSaveInstanceState} method * will be called). Note that even if freezing is enabled, the - * view still must have an id assigned to it (via {@link #setId setId()}) + * view still must have an id assigned to it (via {@link #setId(int)}) * for its state to be saved. This flag can only disable the * saving of this view; any child views may still have their state saved. * @@ -4355,7 +4588,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * false), or if it is focusable and it is not focusable in touch mode * ({@link #isFocusableInTouchMode}) while the device is in touch mode. * - * See also {@link #focusSearch}, which is what you call to say that you + * See also {@link #focusSearch(int)}, which is what you call to say that you * have focus, and you want your parent to look for the next one. * * This is equivalent to calling {@link #requestFocus(int, Rect)} with arguments @@ -4376,7 +4609,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * false), or if it is focusable and it is not focusable in touch mode * ({@link #isFocusableInTouchMode}) while the device is in touch mode. * - * See also {@link #focusSearch}, which is what you call to say that you + * See also {@link #focusSearch(int)}, which is what you call to say that you * have focus, and you want your parent to look for the next one. * * This is equivalent to calling {@link #requestFocus(int, Rect)} with @@ -4406,7 +4639,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * {@link android.view.ViewGroup#getDescendantFocusability()} equal to * {@link ViewGroup#FOCUS_BLOCK_DESCENDANTS}. * - * See also {@link #focusSearch}, which is what you call to say that you + * See also {@link #focusSearch(int)}, which is what you call to say that you * have focus, and you want your parent to look for the next one. * * You may wish to override this method if your custom {@link View} has an internal @@ -4440,10 +4673,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility return true; } - /** Gets the ViewRoot, or null if not attached. */ - /*package*/ ViewRoot getViewRoot() { + /** Gets the ViewAncestor, or null if not attached. */ + /*package*/ ViewAncestor getViewAncestor() { View root = getRootView(); - return root != null ? (ViewRoot)root.getParent() : null; + return root != null ? (ViewAncestor)root.getParent() : null; } /** @@ -4459,7 +4692,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility public final boolean requestFocusFromTouch() { // Leave touch mode if we need to if (isInTouchMode()) { - ViewRoot viewRoot = getViewRoot(); + ViewAncestor viewRoot = getViewAncestor(); if (viewRoot != null) { viewRoot.ensureTouchMode(false); } @@ -4516,22 +4749,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * capture information of this view for later analysis: developement only - * check dynamic switch to make sure we only dump view - * when ViewDebug.SYSTEM_PROPERTY_CAPTURE_VIEW) is set - */ - private static void captureViewInfo(String subTag, View v) { - if (v == null || SystemProperties.getInt(ViewDebug.SYSTEM_PROPERTY_CAPTURE_VIEW, 0) == 0) { - return; - } - ViewDebug.dumpCapturedView(subTag, v); - } - - /** * Return the global {@link KeyEvent.DispatcherState KeyEvent.DispatcherState} * for this view's window. Returns null if the view is not currently attached * to the window. Normally you will not need to use this directly, but - * just use the standard high-level event callbacks like {@link #onKeyDown}. + * just use the standard high-level event callbacks like + * {@link #onKeyDown(int, KeyEvent)}. */ public KeyEvent.DispatcherState getKeyDispatcherState() { return mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null; @@ -4562,21 +4784,26 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return True if the event was handled, false otherwise. */ public boolean dispatchKeyEvent(KeyEvent event) { - // If any attached key listener a first crack at the event. - - //noinspection SimplifiableIfStatement,deprecation - if (android.util.Config.LOGV) { - captureViewInfo("captureViewKeyEvent", this); + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onKeyEvent(event, 0); } + // Give any attached key listener a first crack at the event. //noinspection SimplifiableIfStatement if (mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnKeyListener.onKey(this, event.getKeyCode(), event)) { return true; } - return event.dispatch(this, mAttachInfo != null - ? mAttachInfo.mKeyDispatchState : null, this); + if (event.dispatch(this, mAttachInfo != null + ? mAttachInfo.mKeyDispatchState : null, this)) { + return true; + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); + } + return false; } /** @@ -4597,16 +4824,26 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event) { - if (!onFilterTouchEventForSecurity(event)) { - return false; + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTouchEvent(event, 0); } - //noinspection SimplifiableIfStatement - if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && - mOnTouchListener.onTouch(this, event)) { - return true; + if (onFilterTouchEventForSecurity(event)) { + //noinspection SimplifiableIfStatement + if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && + mOnTouchListener.onTouch(this, event)) { + return true; + } + + if (onTouchEvent(event)) { + return true; + } + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } - return onTouchEvent(event); + return false; } /** @@ -4634,8 +4871,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTrackballEvent(MotionEvent event) { + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTrackballEvent(event, 0); + } + //Log.i("view", "view=" + this + ", " + event.toString()); - return onTrackballEvent(event); + if (onTrackballEvent(event)) { + return true; + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); + } + return false; } /** @@ -4643,28 +4891,101 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * <p> * Generic motion events with source class {@link InputDevice#SOURCE_CLASS_POINTER} * are delivered to the view under the pointer. All other generic motion events are - * delivered to the focused view. + * delivered to the focused view. Hover events are handled specially and are delivered + * to {@link #onHoverEvent(MotionEvent)}. * </p> * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onGenericMotionEvent(event, 0); + } + + final int source = event.getSource(); + if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { + final int action = event.getAction(); + if (action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_MOVE + || action == MotionEvent.ACTION_HOVER_EXIT) { + if (dispatchHoverEvent(event)) { + return true; + } + } else if (dispatchGenericPointerEvent(event)) { + return true; + } + } else if (dispatchGenericFocusedEvent(event)) { + return true; + } + //noinspection SimplifiableIfStatement if (mOnGenericMotionListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnGenericMotionListener.onGenericMotion(this, event)) { return true; } - return onGenericMotionEvent(event); + if (onGenericMotionEvent(event)) { + return true; + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); + } + return false; + } + + /** + * Dispatch a hover event. + * <p> + * Do not call this method directly. + * Call {@link #dispatchGenericMotionEvent(MotionEvent)} instead. + * </p> + * + * @param event The motion event to be dispatched. + * @return True if the event was handled by the view, false otherwise. + * @hide + */ + protected boolean dispatchHoverEvent(MotionEvent event) { + return onHoverEvent(event); + } + + /** + * Dispatch a generic motion event to the view under the first pointer. + * <p> + * Do not call this method directly. + * Call {@link #dispatchGenericMotionEvent(MotionEvent)} instead. + * </p> + * + * @param event The motion event to be dispatched. + * @return True if the event was handled by the view, false otherwise. + * @hide + */ + protected boolean dispatchGenericPointerEvent(MotionEvent event) { + return false; + } + + /** + * Dispatch a generic motion event to the currently focused view. + * <p> + * Do not call this method directly. + * Call {@link #dispatchGenericMotionEvent(MotionEvent)} instead. + * </p> + * + * @param event The motion event to be dispatched. + * @return True if the event was handled by the view, false otherwise. + * @hide + */ + protected boolean dispatchGenericFocusedEvent(MotionEvent event) { + return false; } /** * Dispatch a pointer event. * <p> - * Dispatches touch related pointer events to {@link #onTouchEvent} and all - * other events to {@link #onGenericMotionEvent}. This separation of concerns - * reinforces the invariant that {@link #onTouchEvent} is really about touches + * Dispatches touch related pointer events to {@link #onTouchEvent(MotionEvent)} and all + * other events to {@link #onGenericMotionEvent(MotionEvent)}. This separation of concerns + * reinforces the invariant that {@link #onTouchEvent(MotionEvent)} is really about touches * and should not be expected to handle other pointing device features. * </p> * @@ -4789,7 +5110,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @param visibility The new visibility of the window. * - * @see #onWindowVisibilityChanged + * @see #onWindowVisibilityChanged(int) */ public void dispatchWindowVisibilityChanged(int visibility) { onWindowVisibilityChanged(visibility); @@ -4865,7 +5186,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @param newConfig The new resource configuration. * - * @see #onConfigurationChanged + * @see #onConfigurationChanged(android.content.res.Configuration) */ public void dispatchConfigurationChanged(Configuration newConfig) { onConfigurationChanged(newConfig); @@ -4926,7 +5247,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (mAttachInfo != null) { return mAttachInfo.mInTouchMode; } else { - return ViewRoot.isInTouchMode(); + return ViewAncestor.isInTouchMode(); } } @@ -4981,9 +5302,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) && (event.getRepeatCount() == 0)) { setPressed(true); - if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { - postCheckForLongClick(0); - } + checkForLongClick(0); return true; } break; @@ -5223,15 +5542,80 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * </code> * * @param event The generic motion event being processed. - * - * @return Return true if you have consumed the event, false if you haven't. - * The default implementation always returns false. + * @return True if the event was handled, false otherwise. */ public boolean onGenericMotionEvent(MotionEvent event) { return false; } /** + * Implement this method to handle hover events. + * <p> + * Hover events are pointer events with action {@link MotionEvent#ACTION_HOVER_ENTER}, + * {@link MotionEvent#ACTION_HOVER_MOVE}, or {@link MotionEvent#ACTION_HOVER_EXIT}. + * </p><p> + * The view receives hover enter as the pointer enters the bounds of the view and hover + * exit as the pointer exits the bound of the view or just before the pointer goes down + * (which implies that {@link #onTouchEvent(MotionEvent)} will be called soon). + * </p><p> + * If the view would like to handle the hover event itself and prevent its children + * from receiving hover, it should return true from this method. If this method returns + * true and a child has already received a hover enter event, the child will + * automatically receive a hover exit event. + * </p><p> + * The default implementation sets the hovered state of the view if the view is + * clickable. + * </p> + * + * @param event The motion event that describes the hover. + * @return True if this view handled the hover event and does not want its children + * to receive the hover event. + */ + public boolean onHoverEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + setHovered(true); + break; + + case MotionEvent.ACTION_HOVER_EXIT: + setHovered(false); + break; + } + + return false; + } + + /** + * Returns true if the view is currently hovered. + * + * @return True if the view is currently hovered. + */ + public boolean isHovered() { + return (mPrivateFlags & HOVERED) != 0; + } + + /** + * Sets whether the view is currently hovered. + * + * @param hovered True if the view is hovered. + */ + public void setHovered(boolean hovered) { + if (hovered) { + if ((mPrivateFlags & HOVERED) == 0) { + mPrivateFlags |= HOVERED; + refreshDrawableState(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + } + } else { + if ((mPrivateFlags & HOVERED) != 0) { + mPrivateFlags &= ~HOVERED; + refreshDrawableState(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + } + } + } + + /** * Implement this method to handle touch screen motion events. * * @param event The motion event. @@ -5241,6 +5625,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { + if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) { + mPrivateFlags &= ~PRESSED; + refreshDrawableState(); + } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || @@ -5309,12 +5697,37 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility break; case MotionEvent.ACTION_DOWN: - if (mPendingCheckForTap == null) { - mPendingCheckForTap = new CheckForTap(); - } - mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; - postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + + if (performButtonActionOnTouchDown(event)) { + break; + } + + // Walk up the hierarchy to determine if we're inside a scrolling container. + boolean isInScrollingContainer = false; + ViewParent p = getParent(); + while (p != null && p instanceof ViewGroup) { + if (((ViewGroup) p).shouldDelayChildPressedState()) { + isInScrollingContainer = true; + break; + } + p = p.getParent(); + } + + // For views inside a scrolling container, delay the pressed feedback for + // a short period in case this is a scroll. + if (isInScrollingContainer) { + mPrivateFlags |= PREPRESSED; + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + } else { + // Not inside a scrolling container, so show the feedback right away + mPrivateFlags |= PRESSED; + refreshDrawableState(); + checkForLongClick(0); + } break; case MotionEvent.ACTION_CANCEL: @@ -5544,6 +5957,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mParent.recomputeViewAttributes(this); } } + + if ((changed & HORIZONTAL_DIRECTION_MASK) != 0) { + requestLayout(); + } } /** @@ -5700,7 +6117,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Return the full width measurement information for this view as computed - * by the most recent call to {@link #measure}. This result is a bit mask + * by the most recent call to {@link #measure(int, int)}. This result is a bit mask * as defined by {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. * This should be used during measurement and layout calculations only. Use * {@link #getWidth()} to see how wide a view is after layout. @@ -5724,7 +6141,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Return the full height measurement information for this view as computed - * by the most recent call to {@link #measure}. This result is a bit mask + * by the most recent call to {@link #measure(int, int)}. This result is a bit mask * as defined by {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. * This should be used during measurement and layout calculations only. Use * {@link #getHeight()} to see how wide a view is after layout. @@ -6693,9 +7110,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * When a view has focus and the user navigates away from it, the next view is searched for * starting from the rectangle filled in by this method. * - * By default, the rectange is the {@link #getDrawingRect})of the view. However, if your - * view maintains some idea of internal selection, such as a cursor, or a selected row - * or column, you should override this method and fill in a more specific rectangle. + * By default, the rectange is the {@link #getDrawingRect(android.graphics.Rect)}) + * of the view. However, if your view maintains some idea of internal selection, + * such as a cursor, or a selected row or column, you should override this method and + * fill in a more specific rectangle. * * @param r The rectangle to fill in, in this view's coordinates. */ @@ -7062,9 +7480,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Mark the the area defined by dirty as needing to be drawn. If the view is - * visible, {@link #onDraw} will be called at some point in the future. - * This must be called from a UI thread. To call from a non-UI thread, call - * {@link #postInvalidate()}. + * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some point + * in the future. This must be called from a UI thread. To call from a non-UI + * thread, call {@link #postInvalidate()}. * * WARNING: This method is destructive to dirty. * @param dirty the rectangle representing the bounds of the dirty region @@ -7085,7 +7503,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { if (p != null && ai != null && ai.mHardwareAccelerated) { // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewRoot to redraw everything + // with a null dirty rect, which tells the ViewAncestor to redraw everything p.invalidateChild(this, null); return; } @@ -7104,9 +7522,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Mark the the area defined by the rect (l,t,r,b) as needing to be drawn. * The coordinates of the dirty rect are relative to the view. - * If the view is visible, {@link #onDraw} will be called at some point - * in the future. This must be called from a UI thread. To call - * from a non-UI thread, call {@link #postInvalidate()}. + * If the view is visible, {@link #onDraw(android.graphics.Canvas)} + * will be called at some point in the future. This must be called from + * a UI thread. To call from a non-UI thread, call {@link #postInvalidate()}. * @param l the left position of the dirty region * @param t the top position of the dirty region * @param r the right position of the dirty region @@ -7128,7 +7546,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { if (p != null && ai != null && ai.mHardwareAccelerated) { // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewRoot to redraw everything + // with a null dirty rect, which tells the ViewAncestor to redraw everything p.invalidateChild(this, null); return; } @@ -7144,9 +7562,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * Invalidate the whole view. If the view is visible, {@link #onDraw} will - * be called at some point in the future. This must be called from a - * UI thread. To call from a non-UI thread, call {@link #postInvalidate()}. + * Invalidate the whole view. If the view is visible, + * {@link #onDraw(android.graphics.Canvas)} will be called at some point in + * the future. This must be called from a UI thread. To call from a non-UI thread, + * call {@link #postInvalidate()}. */ public void invalidate() { invalidate(true); @@ -7183,7 +7602,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { if (p != null && ai != null && ai.mHardwareAccelerated) { // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewRoot to redraw everything + // with a null dirty rect, which tells the ViewAncestor to redraw everything p.invalidateChild(this, null); return; } @@ -7212,8 +7631,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mPrivateFlags &= ~DRAWN; mPrivateFlags |= INVALIDATED; mPrivateFlags &= ~DRAWING_CACHE_VALID; - if (mParent != null && mAttachInfo != null && mAttachInfo.mHardwareAccelerated) { - mParent.invalidateChild(this, null); + if (mParent != null && mAttachInfo != null) { + if (mAttachInfo.mHardwareAccelerated) { + mParent.invalidateChild(this, null); + } else { + final Rect r = mAttachInfo.mTmpInvalRect; + r.set(0, 0, mRight - mLeft, mBottom - mTop); + // Don't call invalidate -- we don't want to internally scroll + // our own bounds + mParent.invalidateChild(this, r); + } } } } @@ -7319,11 +7746,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ public boolean post(Runnable action) { Handler handler; - if (mAttachInfo != null) { - handler = mAttachInfo.mHandler; + AttachInfo attachInfo = mAttachInfo; + if (attachInfo != null) { + handler = attachInfo.mHandler; } else { // Assume that post will succeed later - ViewRoot.getRunQueue().post(action); + ViewAncestor.getRunQueue().post(action); return true; } @@ -7348,11 +7776,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ public boolean postDelayed(Runnable action, long delayMillis) { Handler handler; - if (mAttachInfo != null) { - handler = mAttachInfo.mHandler; + AttachInfo attachInfo = mAttachInfo; + if (attachInfo != null) { + handler = attachInfo.mHandler; } else { // Assume that post will succeed later - ViewRoot.getRunQueue().postDelayed(action, delayMillis); + ViewAncestor.getRunQueue().postDelayed(action, delayMillis); return true; } @@ -7371,11 +7800,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ public boolean removeCallbacks(Runnable action) { Handler handler; - if (mAttachInfo != null) { - handler = mAttachInfo.mHandler; + AttachInfo attachInfo = mAttachInfo; + if (attachInfo != null) { + handler = attachInfo.mHandler; } else { // Assume that post will succeed later - ViewRoot.getRunQueue().removeCallbacks(action); + ViewAncestor.getRunQueue().removeCallbacks(action); return true; } @@ -7419,11 +7849,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility public void postInvalidateDelayed(long delayMilliseconds) { // We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window - if (mAttachInfo != null) { + AttachInfo attachInfo = mAttachInfo; + if (attachInfo != null) { Message msg = Message.obtain(); msg.what = AttachInfo.INVALIDATE_MSG; msg.obj = this; - mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds); + attachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds); } } @@ -7443,7 +7874,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility // We try only with the AttachInfo because there's no point in invalidating // if we are not attached to our window - if (mAttachInfo != null) { + AttachInfo attachInfo = mAttachInfo; + if (attachInfo != null) { final AttachInfo.InvalidateInfo info = AttachInfo.InvalidateInfo.acquire(); info.target = this; info.left = left; @@ -7454,7 +7886,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility final Message msg = Message.obtain(); msg.what = AttachInfo.INVALIDATE_RECT_MSG; msg.obj = info; - mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds); + attachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds); } } @@ -8046,9 +8478,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * This is called when the view is attached to a window. At this point it * has a Surface and will start drawing. Note that this function is - * guaranteed to be called before {@link #onDraw}, however it may be called - * any time before the first onDraw -- including before or after - * {@link #onMeasure}. + * guaranteed to be called before {@link #onDraw(android.graphics.Canvas)}, + * however it may be called any time before the first onDraw -- including + * before or after {@link #onMeasure(int, int)}. * * @see #onDetachedFromWindow() */ @@ -8061,6 +8493,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mPrivateFlags &= ~AWAKEN_SCROLL_BARS_ON_ATTACH; } jumpDrawablesToCurrentState(); + + // We are supposing here that the parent directionality will be resolved before its children + // View horizontalDirection public attribute resolution to an internal var. + // Resolving the layout direction. LTR is set initially. + mPrivateFlags2 &= ~RESOLVED_LAYOUT_RTL; + switch (getHorizontalDirection()) { + case HORIZONTAL_DIRECTION_INHERIT: + // If this is root view, no need to look at parent's layout dir. + if (mParent != null && mParent instanceof ViewGroup && + ((ViewGroup) mParent).isLayoutRtl()) { + mPrivateFlags2 |= RESOLVED_LAYOUT_RTL; + } + break; + case HORIZONTAL_DIRECTION_RTL: + mPrivateFlags2 |= RESOLVED_LAYOUT_RTL; + break; + } } /** @@ -8221,24 +8670,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @param container The SparseArray in which to save the view's state. * - * @see #restoreHierarchyState - * @see #dispatchSaveInstanceState - * @see #onSaveInstanceState + * @see #restoreHierarchyState(android.util.SparseArray) + * @see #dispatchSaveInstanceState(android.util.SparseArray) + * @see #onSaveInstanceState() */ public void saveHierarchyState(SparseArray<Parcelable> container) { dispatchSaveInstanceState(container); } /** - * Called by {@link #saveHierarchyState} to store the state for this view and its children. - * May be overridden to modify how freezing happens to a view's children; for example, some - * views may want to not store state for their children. + * Called by {@link #saveHierarchyState(android.util.SparseArray)} to store the state for + * this view and its children. May be overridden to modify how freezing happens to a + * view's children; for example, some views may want to not store state for their children. * * @param container The SparseArray in which to save the view's state. * - * @see #dispatchRestoreInstanceState - * @see #saveHierarchyState - * @see #onSaveInstanceState + * @see #dispatchRestoreInstanceState(android.util.SparseArray) + * @see #saveHierarchyState(android.util.SparseArray) + * @see #onSaveInstanceState() */ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) { @@ -8272,9 +8721,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return Returns a Parcelable object containing the view's current dynamic * state, or null if there is nothing interesting to save. The * default implementation returns null. - * @see #onRestoreInstanceState - * @see #saveHierarchyState - * @see #dispatchSaveInstanceState + * @see #onRestoreInstanceState(android.os.Parcelable) + * @see #saveHierarchyState(android.util.SparseArray) + * @see #dispatchSaveInstanceState(android.util.SparseArray) * @see #setSaveEnabled(boolean) */ protected Parcelable onSaveInstanceState() { @@ -8287,24 +8736,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @param container The SparseArray which holds previously frozen states. * - * @see #saveHierarchyState - * @see #dispatchRestoreInstanceState - * @see #onRestoreInstanceState + * @see #saveHierarchyState(android.util.SparseArray) + * @see #dispatchRestoreInstanceState(android.util.SparseArray) + * @see #onRestoreInstanceState(android.os.Parcelable) */ public void restoreHierarchyState(SparseArray<Parcelable> container) { dispatchRestoreInstanceState(container); } /** - * Called by {@link #restoreHierarchyState} to retrieve the state for this view and its - * children. May be overridden to modify how restoreing happens to a view's children; for - * example, some views may want to not store state for their children. + * Called by {@link #restoreHierarchyState(android.util.SparseArray)} to retrieve the + * state for this view and its children. May be overridden to modify how restoring + * happens to a view's children; for example, some views may want to not store state + * for their children. * * @param container The SparseArray which holds previously saved state. * - * @see #dispatchSaveInstanceState - * @see #restoreHierarchyState - * @see #onRestoreInstanceState + * @see #dispatchSaveInstanceState(android.util.SparseArray) + * @see #restoreHierarchyState(android.util.SparseArray) + * @see #onRestoreInstanceState(android.os.Parcelable) */ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { if (mID != NO_ID) { @@ -8330,9 +8780,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @param state The frozen state that had previously been returned by * {@link #onSaveInstanceState}. * - * @see #onSaveInstanceState - * @see #restoreHierarchyState - * @see #dispatchRestoreInstanceState + * @see #onSaveInstanceState() + * @see #restoreHierarchyState(android.util.SparseArray) + * @see #dispatchRestoreInstanceState(android.util.SparseArray) */ protected void onRestoreInstanceState(Parcelable state) { mPrivateFlags |= SAVE_STATE_CALLED; @@ -8448,6 +8898,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility // Destroy any previous software drawing cache if needed switch (mLayerType) { + case LAYER_TYPE_HARDWARE: + if (mHardwareLayer != null) { + mHardwareLayer.destroy(); + mHardwareLayer = null; + } + // fall through - unaccelerated views may use software layer mechanism instead case LAYER_TYPE_SOFTWARE: if (mDrawingCache != null) { mDrawingCache.recycle(); @@ -8459,12 +8915,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mUnscaledDrawingCache = null; } break; - case LAYER_TYPE_HARDWARE: - if (mHardwareLayer != null) { - mHardwareLayer.destroy(); - mHardwareLayer = null; - } - break; default: break; } @@ -9218,9 +9668,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Manually render this view (and all of its children) to the given Canvas. * The view must have already done a full layout before this function is - * called. When implementing a view, implement {@link #onDraw} instead of - * overriding this method. If you do need to override this method, call - * the superclass version. + * called. When implementing a view, implement + * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. + * If you do need to override this method, call the superclass version. * * @param canvas The Canvas to which the View is rendered. */ @@ -9433,8 +9883,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * optimize the drawing of the fading edges. If you do return a non-zero color, the alpha * should be set to 0xFF. * - * @see #setVerticalFadingEdgeEnabled - * @see #setHorizontalFadingEdgeEnabled + * @see #setVerticalFadingEdgeEnabled(boolean) + * @see #setHorizontalFadingEdgeEnabled(boolean) * * @return The known solid color background for this view, or 0 if the color may vary */ @@ -9547,6 +9997,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * <p>Indicates whether or not this view's layout is right-to-left. This is resolved from + * layout attribute and/or the inherited value from the parent.</p> + * + * @return true if the layout is right-to-left. + */ + public boolean isLayoutRtl() { + return (mPrivateFlags2 & RESOLVED_LAYOUT_RTL) == RESOLVED_LAYOUT_RTL; + } + + /** * Assign a size and position to a view and all of its * descendants * @@ -9774,8 +10234,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return boolean If true than the Drawable is being displayed in the * view; else false and it is not allowed to animate. * - * @see #unscheduleDrawable - * @see #drawableStateChanged + * @see #unscheduleDrawable(android.graphics.drawable.Drawable) + * @see #drawableStateChanged() */ protected boolean verifyDrawable(Drawable who) { return who == mBGDrawable; @@ -9788,7 +10248,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * <p>Be sure to call through to the superclass when overriding this * function. * - * @see Drawable#setState + * @see Drawable#setState(int[]) */ protected void drawableStateChanged() { Drawable d = mBGDrawable; @@ -9821,9 +10281,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @return The current drawable state * - * @see Drawable#setState - * @see #drawableStateChanged - * @see #onCreateDrawableState + * @see Drawable#setState(int[]) + * @see #drawableStateChanged() + * @see #onCreateDrawableState(int) */ public final int[] getDrawableState() { if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) { @@ -9848,7 +10308,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return Returns an array holding the current {@link Drawable} state of * the view. * - * @see #mergeDrawableStates + * @see #mergeDrawableStates(int[], int[]) */ protected int[] onCreateDrawableState(int extraSpace) { if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE && @@ -9867,12 +10327,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if ((privateFlags & SELECTED) != 0) viewStateIndex |= VIEW_STATE_SELECTED; if (hasWindowFocus()) viewStateIndex |= VIEW_STATE_WINDOW_FOCUSED; if ((privateFlags & ACTIVATED) != 0) viewStateIndex |= VIEW_STATE_ACTIVATED; - if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested) { + if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested && + HardwareRenderer.isAvailable()) { // This is set if HW acceleration is requested, even if the current // process doesn't allow it. This is just to allow app preview // windows to better match their app. viewStateIndex |= VIEW_STATE_ACCELERATED; } + if ((privateFlags & HOVERED) != 0) viewStateIndex |= VIEW_STATE_HOVERED; + + final int privateFlags2 = mPrivateFlags2; + if ((privateFlags2 & DRAG_CAN_ACCEPT) != 0) viewStateIndex |= VIEW_STATE_DRAG_CAN_ACCEPT; + if ((privateFlags2 & DRAG_HOVERED) != 0) viewStateIndex |= VIEW_STATE_DRAG_HOVERED; drawableState = VIEW_STATE_SETS[viewStateIndex]; @@ -9906,10 +10372,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Merge your own state values in <var>additionalState</var> into the base * state values <var>baseState</var> that were returned by - * {@link #onCreateDrawableState}. + * {@link #onCreateDrawableState(int)}. * * @param baseState The base state values returned by - * {@link #onCreateDrawableState}, which will be modified to also hold your + * {@link #onCreateDrawableState(int)}, which will be modified to also hold your * own additional state values. * * @param additionalState The additional state values you would like @@ -9918,7 +10384,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return As a convenience, the <var>baseState</var> array you originally * passed into the function is returned. * - * @see #onCreateDrawableState + * @see #onCreateDrawableState(int) */ protected static int[] mergeDrawableStates(int[] baseState, int[] additionalState) { final int N = baseState.length; @@ -10348,9 +10814,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility viewParent = view.mParent; } - if (viewParent instanceof ViewRoot) { + if (viewParent instanceof ViewAncestor) { // *cough* - final ViewRoot vr = (ViewRoot)viewParent; + final ViewAncestor vr = (ViewAncestor)viewParent; location[1] -= vr.mCurScrollY; } } @@ -10437,8 +10903,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * number. * * @see #NO_ID - * @see #getId - * @see #findViewById + * @see #getId() + * @see #findViewById(int) * * @param id a number used to identify the view * @@ -10477,8 +10943,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return a positive integer used to identify the view or {@link #NO_ID} * if the view has no ID * - * @see #setId - * @see #findViewById + * @see #setId(int) + * @see #findViewById(int) * @attr ref android.R.styleable#View_id */ @ViewDebug.CapturedViewProperty @@ -11160,7 +11626,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * therefore all View objects remove themselves from the global transparent * region (passed as a parameter to this function). * - * @param region The transparent region for this ViewRoot (window). + * @param region The transparent region for this ViewAncestor (window). * * @return Returns true if the effective visibility of the view at this * point is opaque, regardless of the transparent region; returns false @@ -11341,6 +11807,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @return The View object associate with this builder object. */ + @SuppressWarnings({"JavadocReference"}) final public View getView() { return mView.get(); } @@ -11466,7 +11933,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility surface.unlockCanvasAndPost(canvas); } - final ViewRoot root = getViewRoot(); + final ViewAncestor root = getViewAncestor(); // Cache the local state object for delivery with DragEvents root.setLocalDragState(myLocalState); @@ -11540,6 +12007,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility return onDragEvent(event); } + boolean canAcceptDrag() { + return (mPrivateFlags2 & DRAG_CAN_ACCEPT) != 0; + } + /** * This needs to be a better API (NOT ON VIEW) before it is exposed. If * it is ever exposed at all. @@ -11550,7 +12021,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Given a Drawable whose bounds have been set to draw into this view, - * update a Region being computed for {@link #gatherTransparentRegion} so + * update a Region being computed for + * {@link #gatherTransparentRegion(android.graphics.Region)} so * that any non-transparent parts of the Drawable are removed from the * given transparent region. * @@ -11597,15 +12069,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } } - private void postCheckForLongClick(int delayOffset) { - mHasPerformedLongPress = false; + private void checkForLongClick(int delayOffset) { + if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { + mHasPerformedLongPress = false; - if (mPendingCheckForLongPress == null) { - mPendingCheckForLongPress = new CheckForLongPress(); + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + mPendingCheckForLongPress.rememberWindowAttachCount(); + postDelayed(mPendingCheckForLongPress, + ViewConfiguration.getLongPressTimeout() - delayOffset); } - mPendingCheckForLongPress.rememberWindowAttachCount(); - postDelayed(mPendingCheckForLongPress, - ViewConfiguration.getLongPressTimeout() - delayOffset); } /** @@ -11917,9 +12391,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mPrivateFlags &= ~PREPRESSED; mPrivateFlags |= PRESSED; refreshDrawableState(); - if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { - postCheckForLongClick(ViewConfiguration.getTapTimeout()); - } + checkForLongClick(ViewConfiguration.getTapTimeout()); } } @@ -12084,12 +12556,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * Interface definition for a callback to be invoked when the status bar changes * visibility. * - * @see #setOnSystemUiVisibilityChangeListener + * @see View#setOnSystemUiVisibilityChangeListener(android.view.View.OnSystemUiVisibilityChangeListener) */ public interface OnSystemUiVisibilityChangeListener { /** * Called when the status bar changes visibility because of a call to - * {@link #setSystemUiVisibility}. + * {@link View#setSystemUiVisibility(int)}. * * @param visibility {@link #STATUS_BAR_VISIBLE} or {@link #STATUS_BAR_HIDDEN}. */ @@ -12248,7 +12720,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility boolean mScalingRequired; /** - * If set, ViewRoot doesn't use its lame animation for when the window resizes. + * If set, ViewAncestor doesn't use its lame animation for when the window resizes. */ boolean mTurnOffWindowResizeAnim; @@ -12327,7 +12799,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility boolean mInTouchMode; /** - * Indicates that ViewRoot should trigger a global layout change + * Indicates that ViewAncestor should trigger a global layout change * the next time it performs a traversal */ boolean mRecomputeGlobalAttributes; @@ -12389,7 +12861,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility Canvas mCanvas; /** - * A Handler supplied by a view's {@link android.view.ViewRoot}. This + * A Handler supplied by a view's {@link android.view.ViewAncestor}. This * handler can be used to pump events in the UI events queue. */ final Handler mHandler; diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewAncestor.java index 2ba28c6..8085ea8 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewAncestor.java @@ -49,7 +49,6 @@ import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; import android.util.AndroidRuntimeException; -import android.util.Config; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; @@ -83,9 +82,9 @@ import java.util.ArrayList; * {@hide} */ @SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) -public final class ViewRoot extends Handler implements ViewParent, +public final class ViewAncestor extends Handler implements ViewParent, View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { - private static final String TAG = "ViewRoot"; + private static final String TAG = "ViewAncestor"; private static final boolean DBG = false; private static final boolean SHOW_FPS = false; private static final boolean LOCAL_LOGV = false; @@ -186,6 +185,8 @@ public final class ViewRoot extends Handler implements ViewParent, final Rect mVisRect; // used to retrieve visible rect of focused view. boolean mTraversalScheduled; + long mLastTraversalFinishedTimeNanos; + long mLastDrawDurationNanos; boolean mWillDrawSoon; boolean mLayoutRequested; boolean mFirst; @@ -250,6 +251,13 @@ public final class ViewRoot extends Handler implements ViewParent, private final int mDensity; + /** + * Consistency verifier for debugging purposes. + */ + protected final InputEventConsistencyVerifier mInputEventConsistencyVerifier = + InputEventConsistencyVerifier.isInstrumentationEnabled() ? + new InputEventConsistencyVerifier(this, 0) : null; + public static IWindowSession getWindowSession(Looper mainLooper) { synchronized (mStaticInit) { if (!mInitialized) { @@ -265,7 +273,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } - public ViewRoot(Context context) { + public ViewAncestor(Context context) { super(); if (MEASURE_LATENCY) { @@ -500,9 +508,14 @@ public final class ViewRoot extends Handler implements ViewParent, final boolean hardwareAccelerated = (attrs.flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0; - if (attrs != null && hardwareAccelerated) { + if (hardwareAccelerated) { + if (!HardwareRenderer.isAvailable()) { + mAttachInfo.mHardwareAccelerationRequested = true; + return; + } + // Only enable hardware acceleration if we are not in the system process - // The window manager creates ViewRoots to display animated preview windows + // The window manager creates ViewAncestors to display animated preview windows // of launching apps and we don't want those to be hardware accelerated final boolean systemHwAccelerated = @@ -523,8 +536,6 @@ public final class ViewRoot extends Handler implements ViewParent, mAttachInfo.mHardwareRenderer = HardwareRenderer.createGlRenderer(2, translucent); mAttachInfo.mHardwareAccelerated = mAttachInfo.mHardwareAccelerationRequested = mAttachInfo.mHardwareRenderer != null; - } else if (HardwareRenderer.isAvailable()) { - mAttachInfo.mHardwareAccelerationRequested = true; } } } @@ -660,6 +671,14 @@ public final class ViewRoot extends Handler implements ViewParent, public void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; + + if (ViewDebug.DEBUG_LATENCY && mLastTraversalFinishedTimeNanos != 0) { + final long now = System.nanoTime(); + Log.d(TAG, "Latency: Scheduled traversal, it has been " + + ((now - mLastTraversalFinishedTimeNanos) * 0.000001f) + + "ms since the last traversal finished."); + } + sendEmptyMessage(DO_TRAVERSAL); } } @@ -1252,7 +1271,7 @@ public final class ViewRoot extends Handler implements ViewParent, } host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); - if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) { + if (false && ViewDebug.consistencyCheckEnabled) { if (!host.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_LAYOUT)) { throw new IllegalStateException("The view hierarchy is an inconsistent state," + "please refer to the logs with the tag " @@ -1378,8 +1397,18 @@ public final class ViewRoot extends Handler implements ViewParent, if (!cancelDraw && !newSurface) { mFullRedrawNeeded = false; + + final long drawStartTime; + if (ViewDebug.DEBUG_LATENCY) { + drawStartTime = System.nanoTime(); + } + draw(fullRedrawNeeded); + if (ViewDebug.DEBUG_LATENCY) { + mLastDrawDurationNanos = System.nanoTime() - drawStartTime; + } + if ((relayoutResult&WindowManagerImpl.RELAYOUT_FIRST_TIME) != 0 || mReportNextDraw) { if (LOCAL_LOGV) { @@ -1478,6 +1507,20 @@ public final class ViewRoot extends Handler implements ViewParent, } } + /** + * @hide + */ + void outputDisplayList(View view) { + if (mAttachInfo != null && mAttachInfo.mHardwareCanvas != null) { + + HardwareCanvas canvas = (HardwareCanvas) mAttachInfo.mHardwareCanvas; + DisplayList displayList = view.getDisplayList(); + if (displayList != null) { + canvas.outputDisplayList(displayList); + } + } + } + private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; if (surface == null || !surface.isValid()) { @@ -1590,8 +1633,20 @@ public final class ViewRoot extends Handler implements ViewParent, int right = dirty.right; int bottom = dirty.bottom; + final long lockCanvasStartTime; + if (ViewDebug.DEBUG_LATENCY) { + lockCanvasStartTime = System.nanoTime(); + } + canvas = surface.lockCanvas(dirty); + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(TAG, "Latency: Spent " + + ((now - lockCanvasStartTime) * 0.000001f) + + "ms waiting for surface.lockCanvas()"); + } + if (left != dirty.left || top != dirty.top || right != dirty.right || bottom != dirty.bottom) { mAttachInfo.mIgnoreDirtyState = true; @@ -1668,7 +1723,7 @@ public final class ViewRoot extends Handler implements ViewParent, mAttachInfo.mIgnoreDirtyState = false; } - if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) { + if (false && ViewDebug.consistencyCheckEnabled) { mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING); } @@ -1997,11 +2052,27 @@ public final class ViewRoot extends Handler implements ViewParent, break; case DO_TRAVERSAL: if (mProfile) { - Debug.startMethodTracing("ViewRoot"); + Debug.startMethodTracing("ViewAncestor"); + } + + final long traversalStartTime; + if (ViewDebug.DEBUG_LATENCY) { + traversalStartTime = System.nanoTime(); + mLastDrawDurationNanos = 0; } performTraversals(); + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(TAG, "Latency: Spent " + + ((now - traversalStartTime) * 0.000001f) + + "ms in performTraversals(), with " + + (mLastDrawDurationNanos * 0.000001f) + + "ms of that time in draw()"); + mLastTraversalFinishedTimeNanos = now; + } + if (mProfile) { Debug.stopMethodTracing(); mProfile = false; @@ -2169,25 +2240,68 @@ public final class ViewRoot extends Handler implements ViewParent, } } - private void startInputEvent(InputQueue.FinishedCallback finishedCallback) { + private void startInputEvent(InputEvent event, InputQueue.FinishedCallback finishedCallback) { if (mFinishedCallback != null) { Slog.w(TAG, "Received a new input event from the input queue but there is " + "already an unfinished input event in progress."); } + if (ViewDebug.DEBUG_LATENCY) { + mInputEventReceiveTimeNanos = System.nanoTime(); + mInputEventDeliverTimeNanos = 0; + mInputEventDeliverPostImeTimeNanos = 0; + } + mFinishedCallback = finishedCallback; } - private void finishInputEvent(boolean handled) { + private void finishInputEvent(InputEvent event, boolean handled) { if (LOCAL_LOGV) Log.v(TAG, "Telling window manager input event is finished"); - if (mFinishedCallback != null) { - mFinishedCallback.finished(handled); - mFinishedCallback = null; - } else { + if (mFinishedCallback == null) { Slog.w(TAG, "Attempted to tell the input queue that the current input event " + "is finished but there is no input event actually in progress."); + return; } + + if (ViewDebug.DEBUG_LATENCY) { + final long now = System.nanoTime(); + final long eventTime = event.getEventTimeNano(); + final StringBuilder msg = new StringBuilder(); + msg.append("Latency: Spent "); + msg.append((now - mInputEventReceiveTimeNanos) * 0.000001f); + msg.append("ms processing "); + if (event instanceof KeyEvent) { + final KeyEvent keyEvent = (KeyEvent)event; + msg.append("key event, action="); + msg.append(KeyEvent.actionToString(keyEvent.getAction())); + } else { + final MotionEvent motionEvent = (MotionEvent)event; + msg.append("motion event, action="); + msg.append(MotionEvent.actionToString(motionEvent.getAction())); + msg.append(", historySize="); + msg.append(motionEvent.getHistorySize()); + } + msg.append(", handled="); + msg.append(handled); + msg.append(", received at +"); + msg.append((mInputEventReceiveTimeNanos - eventTime) * 0.000001f); + if (mInputEventDeliverTimeNanos != 0) { + msg.append("ms, delivered at +"); + msg.append((mInputEventDeliverTimeNanos - eventTime) * 0.000001f); + } + if (mInputEventDeliverPostImeTimeNanos != 0) { + msg.append("ms, delivered post IME at +"); + msg.append((mInputEventDeliverPostImeTimeNanos - eventTime) * 0.000001f); + } + msg.append("ms, finished at +"); + msg.append((now - eventTime) * 0.000001f); + msg.append("ms."); + Log.d(TAG, msg.toString()); + } + + mFinishedCallback.finished(handled); + mFinishedCallback = null; } /** @@ -2312,6 +2426,18 @@ public final class ViewRoot extends Handler implements ViewParent, } private void deliverPointerEvent(MotionEvent event, boolean sendDone) { + if (ViewDebug.DEBUG_LATENCY) { + mInputEventDeliverTimeNanos = System.nanoTime(); + } + + if (mInputEventConsistencyVerifier != null) { + if (event.isTouchEvent()) { + mInputEventConsistencyVerifier.onTouchEvent(event, 0); + } else { + mInputEventConsistencyVerifier.onGenericMotionEvent(event, 0); + } + } + // If there is no view, then the event will not be handled. if (mView == null || !mAdded) { finishMotionEvent(event, sendDone, false); @@ -2328,7 +2454,7 @@ public final class ViewRoot extends Handler implements ViewParent, if (isDown) { ensureTouchMode(true); } - if(Config.LOGV) { + if(false) { captureMotionLog("captureDispatchPointer", event); } @@ -2406,7 +2532,7 @@ public final class ViewRoot extends Handler implements ViewParent, private void finishMotionEvent(MotionEvent event, boolean sendDone, boolean handled) { event.recycle(); if (sendDone) { - finishInputEvent(handled); + finishInputEvent(event, handled); } if (LOCAL_LOGV || WATCH_POINTER) { if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { @@ -2416,8 +2542,16 @@ public final class ViewRoot extends Handler implements ViewParent, } private void deliverTrackballEvent(MotionEvent event, boolean sendDone) { + if (ViewDebug.DEBUG_LATENCY) { + mInputEventDeliverTimeNanos = System.nanoTime(); + } + if (DEBUG_TRACKBALL) Log.v(TAG, "Motion event:" + event); + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTrackballEvent(event, 0); + } + // If there is no view, then the event will not be handled. if (mView == null || !mAdded) { finishMotionEvent(event, sendDone, false); @@ -2546,6 +2680,14 @@ public final class ViewRoot extends Handler implements ViewParent, } private void deliverGenericMotionEvent(MotionEvent event, boolean sendDone) { + if (ViewDebug.DEBUG_LATENCY) { + mInputEventDeliverTimeNanos = System.nanoTime(); + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onGenericMotionEvent(event, 0); + } + final int source = event.getSource(); final boolean isJoystick = (source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0; @@ -2781,6 +2923,14 @@ public final class ViewRoot extends Handler implements ViewParent, } private void deliverKeyEvent(KeyEvent event, boolean sendDone) { + if (ViewDebug.DEBUG_LATENCY) { + mInputEventDeliverTimeNanos = System.nanoTime(); + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onKeyEvent(event, 0); + } + // If there is no view, then the event will not be handled. if (mView == null || !mAdded) { finishKeyEvent(event, sendDone, false); @@ -2827,6 +2977,10 @@ public final class ViewRoot extends Handler implements ViewParent, } private void deliverKeyEventPostIme(KeyEvent event, boolean sendDone) { + if (ViewDebug.DEBUG_LATENCY) { + mInputEventDeliverPostImeTimeNanos = System.nanoTime(); + } + // If the view went away, then the event will not be handled. if (mView == null || !mAdded) { finishKeyEvent(event, sendDone, false); @@ -2839,7 +2993,7 @@ public final class ViewRoot extends Handler implements ViewParent, return; } - if (Config.LOGV) { + if (false) { captureKeyLog("captureDispatchKeyEvent", event); } @@ -2940,7 +3094,7 @@ public final class ViewRoot extends Handler implements ViewParent, private void finishKeyEvent(KeyEvent event, boolean sendDone, boolean handled) { if (sendDone) { - finishInputEvent(handled); + finishInputEvent(event, handled); } } @@ -3231,16 +3385,19 @@ public final class ViewRoot extends Handler implements ViewParent, sendMessage(msg); } + private long mInputEventReceiveTimeNanos; + private long mInputEventDeliverTimeNanos; + private long mInputEventDeliverPostImeTimeNanos; private InputQueue.FinishedCallback mFinishedCallback; private final InputHandler mInputHandler = new InputHandler() { public void handleKey(KeyEvent event, InputQueue.FinishedCallback finishedCallback) { - startInputEvent(finishedCallback); + startInputEvent(event, finishedCallback); dispatchKey(event, true); } public void handleMotion(MotionEvent event, InputQueue.FinishedCallback finishedCallback) { - startInputEvent(finishedCallback); + startInputEvent(event, finishedCallback); dispatchMotion(event, true); } }; @@ -3387,6 +3544,14 @@ public final class ViewRoot extends Handler implements ViewParent, public void childDrawableStateChanged(View child) { } + public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) { + if (mView == null) { + return false; + } + AccessibilityManager.getInstance(child.mContext).sendAccessibilityEvent(event); + return true; + } + void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( @@ -3395,7 +3560,7 @@ public final class ViewRoot extends Handler implements ViewParent, } public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { - // ViewRoot never intercepts touch event, so this can be a no-op + // ViewAncestor never intercepts touch event, so this can be a no-op } public boolean requestChildRectangleOnScreen(View child, Rect rectangle, @@ -3444,14 +3609,14 @@ public final class ViewRoot extends Handler implements ViewParent, } static class InputMethodCallback extends IInputMethodCallback.Stub { - private WeakReference<ViewRoot> mViewRoot; + private WeakReference<ViewAncestor> mViewAncestor; - public InputMethodCallback(ViewRoot viewRoot) { - mViewRoot = new WeakReference<ViewRoot>(viewRoot); + public InputMethodCallback(ViewAncestor viewRoot) { + mViewAncestor = new WeakReference<ViewAncestor>(viewRoot); } public void finishedEvent(int seq, boolean handled) { - final ViewRoot viewRoot = mViewRoot.get(); + final ViewAncestor viewRoot = mViewAncestor.get(); if (viewRoot != null) { viewRoot.dispatchFinishedEvent(seq, handled); } @@ -3463,36 +3628,36 @@ public final class ViewRoot extends Handler implements ViewParent, } static class W extends IWindow.Stub { - private final WeakReference<ViewRoot> mViewRoot; + private final WeakReference<ViewAncestor> mViewAncestor; - W(ViewRoot viewRoot) { - mViewRoot = new WeakReference<ViewRoot>(viewRoot); + W(ViewAncestor viewRoot) { + mViewAncestor = new WeakReference<ViewAncestor>(viewRoot); } public void resized(int w, int h, Rect coveredInsets, Rect visibleInsets, boolean reportDraw, Configuration newConfig) { - final ViewRoot viewRoot = mViewRoot.get(); + final ViewAncestor viewRoot = mViewAncestor.get(); if (viewRoot != null) { viewRoot.dispatchResized(w, h, coveredInsets, visibleInsets, reportDraw, newConfig); } } public void dispatchAppVisibility(boolean visible) { - final ViewRoot viewRoot = mViewRoot.get(); + final ViewAncestor viewRoot = mViewAncestor.get(); if (viewRoot != null) { viewRoot.dispatchAppVisibility(visible); } } public void dispatchGetNewSurface() { - final ViewRoot viewRoot = mViewRoot.get(); + final ViewAncestor viewRoot = mViewAncestor.get(); if (viewRoot != null) { viewRoot.dispatchGetNewSurface(); } } public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) { - final ViewRoot viewRoot = mViewRoot.get(); + final ViewAncestor viewRoot = mViewAncestor.get(); if (viewRoot != null) { viewRoot.windowFocusChanged(hasFocus, inTouchMode); } @@ -3512,7 +3677,7 @@ public final class ViewRoot extends Handler implements ViewParent, } public void executeCommand(String command, String parameters, ParcelFileDescriptor out) { - final ViewRoot viewRoot = mViewRoot.get(); + final ViewAncestor viewRoot = mViewAncestor.get(); if (viewRoot != null) { final View view = viewRoot.mView; if (view != null) { @@ -3543,7 +3708,7 @@ public final class ViewRoot extends Handler implements ViewParent, } public void closeSystemDialogs(String reason) { - final ViewRoot viewRoot = mViewRoot.get(); + final ViewAncestor viewRoot = mViewAncestor.get(); if (viewRoot != null) { viewRoot.dispatchCloseSystemDialogs(reason); } @@ -3571,7 +3736,7 @@ public final class ViewRoot extends Handler implements ViewParent, /* Drag/drop */ public void dispatchDragEvent(DragEvent event) { - final ViewRoot viewRoot = mViewRoot.get(); + final ViewAncestor viewRoot = mViewAncestor.get(); if (viewRoot != null) { viewRoot.dispatchDragEvent(event); } @@ -3579,7 +3744,7 @@ public final class ViewRoot extends Handler implements ViewParent, @Override public void dispatchSystemUiVisibilityChanged(int visibility) { - final ViewRoot viewRoot = mViewRoot.get(); + final ViewAncestor viewRoot = mViewAncestor.get(); if (viewRoot != null) { viewRoot.dispatchSystemUiVisibilityChanged(visibility); } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 2a74eb7..36bb046 100755..100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -19,7 +19,6 @@ package android.view; import android.app.AppGlobals; import android.content.Context; import android.content.res.Configuration; -import android.os.Bundle; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.SparseArray; @@ -96,7 +95,7 @@ public class ViewConfiguration { * 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 = 115; + private static final int TAP_TIMEOUT = 180; /** * Defines the duration in milliseconds we will wait to see if a touch event @@ -129,12 +128,6 @@ public class ViewConfiguration { private static final int TOUCH_SLOP = 16; /** - * Distance a touch can wander before we think the user is the first touch - * in a sequence of double tap - */ - private static final int LARGE_TOUCH_SLOP = 18; - - /** * Distance a touch can wander before we think the user is attempting a paged scroll * (in dips) */ @@ -162,6 +155,13 @@ public class ViewConfiguration { private static final int MAXIMUM_FLING_VELOCITY = 8000; /** + * Distance between a touch up event denoting the end of a touch exploration + * gesture and the touch up event of a subsequent tap for the latter tap to be + * considered as a tap i.e. to perform a click. + */ + private static final int TOUCH_EXPLORATION_TAP_SLOP = 80; + + /** * The maximum size of View's drawing cache, expressed in bytes. This size * should be at least equal to the size of the screen in ARGB888 format. */ @@ -189,9 +189,9 @@ public class ViewConfiguration { private final int mMaximumFlingVelocity; private final int mScrollbarSize; private final int mTouchSlop; - private final int mLargeTouchSlop; private final int mPagingTouchSlop; private final int mDoubleTapSlop; + private final int mScaledTouchExplorationTapSlop; private final int mWindowTouchSlop; private final int mMaximumDrawingCacheSize; private final int mOverscrollDistance; @@ -211,9 +211,9 @@ public class ViewConfiguration { mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY; mScrollbarSize = SCROLL_BAR_SIZE; mTouchSlop = TOUCH_SLOP; - mLargeTouchSlop = LARGE_TOUCH_SLOP; mPagingTouchSlop = PAGING_TOUCH_SLOP; mDoubleTapSlop = DOUBLE_TAP_SLOP; + mScaledTouchExplorationTapSlop = TOUCH_EXPLORATION_TAP_SLOP; mWindowTouchSlop = WINDOW_TOUCH_SLOP; //noinspection deprecation mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE; @@ -248,9 +248,9 @@ public class ViewConfiguration { mMaximumFlingVelocity = (int) (density * MAXIMUM_FLING_VELOCITY + 0.5f); mScrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f); mTouchSlop = (int) (sizeAndDensity * TOUCH_SLOP + 0.5f); - mLargeTouchSlop = (int) (sizeAndDensity * LARGE_TOUCH_SLOP + 0.5f); mPagingTouchSlop = (int) (sizeAndDensity * PAGING_TOUCH_SLOP + 0.5f); mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f); + mScaledTouchExplorationTapSlop = (int) (density * TOUCH_EXPLORATION_TAP_SLOP + 0.5f); mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f); // Size of the screen in bytes, in ARGB_8888 format @@ -425,14 +425,6 @@ public class ViewConfiguration { } /** - * @return Distance a touch can wander before we think the user is the first touch - * in a sequence of double tap - */ - public int getScaledLargeTouchSlop() { - return mLargeTouchSlop; - } - - /** * @return Distance a touch can wander before we think the user is scrolling a full page * in dips */ @@ -461,6 +453,17 @@ public class ViewConfiguration { } /** + * @return Distance between a touch up event denoting the end of a touch exploration + * gesture and the touch up event of a subsequent tap for the latter tap to be + * considered as a tap i.e. to perform a click. + * + * @hide + */ + public int getScaledTouchExplorationTapSlop() { + return mScaledTouchExplorationTapSlop; + } + + /** * @return Distance a touch must be outside the bounds of a window for it * to be counted as outside the window for purposes of dismissing that * window. diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index c19a107..4aa8727 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -16,7 +16,6 @@ package android.view; -import android.util.Config; import android.util.Log; import android.util.DisplayMetrics; import android.content.res.Resources; @@ -93,21 +92,6 @@ public class ViewDebug { public static final boolean TRACE_RECYCLER = false; /** - * Enables or disables motion events tracing. Any invoker of - * {@link #trace(View, MotionEvent, MotionEventTraceType)} should first check - * that this value is set to true as not to affect performance. - * - * @hide - */ - public static final boolean TRACE_MOTION_EVENTS = false; - - /** - * The system property of dynamic switch for capturing view information - * when it is set, we dump interested fields and methods for the view on focus - */ - static final String SYSTEM_PROPERTY_CAPTURE_VIEW = "debug.captureview"; - - /** * The system property of dynamic switch for capturing event information * when it is set, we log key events, touch/motion and trackball events */ @@ -141,8 +125,24 @@ public class ViewDebug { public static final boolean DEBUG_DRAG = false; /** + * Enables logging of factors that affect the latency and responsiveness of an application. + * + * Logs the relative difference between the time an event was created and the time it + * was delivered. + * + * Logs the time spent waiting for Surface.lockCanvas() or eglSwapBuffers(). + * This is time that the event loop spends blocked and unresponsive. Ideally, drawing + * and animations should be perfectly synchronized with VSYNC so that swap buffers + * is instantaneous. + * + * Logs the time spent in ViewRoot.performTraversals() or ViewRoot.draw(). + * @hide + */ + public static final boolean DEBUG_LATENCY = false; + + /** * <p>Enables or disables views consistency check. Even when this property is enabled, - * view consistency checks happen only if {@link android.util.Config#DEBUG} is set + * view consistency checks happen only if {@link false} is set * to true. The value of this property can be configured externally in one of the * following files:</p> * <ul> @@ -155,12 +155,6 @@ public class ViewDebug { @Debug.DebugProperty public static boolean consistencyCheckEnabled = false; - static { - if (Config.DEBUG) { - Debug.setFieldsOn(ViewDebug.class, true); - } - } - /** * This annotation can be used to mark fields and methods to be dumped by * the view server. Only non-void methods with no arguments can be annotated @@ -360,6 +354,7 @@ public class ViewDebug { private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT"; private static final String REMOTE_PROFILE = "PROFILE"; private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS"; + private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST"; private static HashMap<Class<?>, Field[]> sFieldsForClasses; private static HashMap<Class<?>, Method[]> sMethodsForClasses; @@ -380,7 +375,7 @@ public class ViewDebug { } private static BufferedWriter sHierarchyTraces; - private static ViewRoot sHierarhcyRoot; + private static ViewAncestor sHierarhcyRoot; private static String sHierarchyTracePrefix; /** @@ -408,21 +403,6 @@ public class ViewDebug { private static String sRecyclerTracePrefix; /** - * Defines the type of motion events trace to output to the motion events traces file. - * - * @hide - */ - public enum MotionEventTraceType { - DISPATCH, - ON_INTERCEPT, - ON_TOUCH - } - - private static BufferedWriter sMotionEventTraces; - private static ViewRoot sMotionEventRoot; - private static String sMotionEventTracePrefix; - - /** * Returns the number of instanciated Views. * * @return The number of Views instanciated in the current process. @@ -434,14 +414,14 @@ public class ViewDebug { } /** - * Returns the number of instanciated ViewRoots. + * Returns the number of instanciated ViewAncestors. * - * @return The number of ViewRoots instanciated in the current process. + * @return The number of ViewAncestors instanciated in the current process. * * @hide */ - public static long getViewRootInstanceCount() { - return Debug.countInstancesOfClass(ViewRoot.class); + public static long getViewAncestorInstanceCount() { + return Debug.countInstancesOfClass(ViewAncestor.class); } /** @@ -558,6 +538,7 @@ public class ViewDebug { recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".traces"); try { if (recyclerDump.exists()) { + //noinspection ResultOfMethodCallIgnored recyclerDump.delete(); } final FileOutputStream file = new FileOutputStream(recyclerDump); @@ -655,7 +636,7 @@ public class ViewDebug { return; } - sHierarhcyRoot = (ViewRoot) view.getRootView().getParent(); + sHierarhcyRoot = (ViewAncestor) view.getRootView().getParent(); } /** @@ -716,146 +697,6 @@ public class ViewDebug { sHierarhcyRoot = null; } - /** - * Outputs a trace to the currently opened traces file. The trace contains the class name - * and instance's hashcode of the specified view as well as the supplied trace type. - * - * @param view the view to trace - * @param event the event of the trace - * @param type the type of the trace - * - * @hide - */ - public static void trace(View view, MotionEvent event, MotionEventTraceType type) { - if (sMotionEventTraces == null) { - return; - } - - try { - sMotionEventTraces.write(type.name()); - sMotionEventTraces.write(' '); - sMotionEventTraces.write(event.getAction()); - sMotionEventTraces.write(' '); - sMotionEventTraces.write(view.getClass().getName()); - sMotionEventTraces.write('@'); - sMotionEventTraces.write(Integer.toHexString(view.hashCode())); - sHierarchyTraces.newLine(); - } catch (IOException e) { - Log.w("View", "Error while dumping trace of event " + event + " for view " + view); - } - } - - /** - * Starts tracing the motion events for the hierarchy of the specificy view. - * The trace is identified by a prefix, used to build the traces files names: - * <code>/EXTERNAL/motion-events/PREFIX.traces</code> and - * <code>/EXTERNAL/motion-events/PREFIX.tree</code>. - * - * Only one view hierarchy can be traced at the same time. After calling this method, any - * other invocation will result in a <code>IllegalStateException</code> unless - * {@link #stopMotionEventTracing()} is invoked before. - * - * Calling this method creates the file <code>/EXTERNAL/motion-events/PREFIX.traces</code> - * containing all the traces (or method calls) relative to the specified view's hierarchy. - * - * This method will return immediately if TRACE_HIERARCHY is false. - * - * @param prefix the traces files name prefix - * @param view the view whose hierarchy must be traced - * - * @see #stopMotionEventTracing() - * @see #trace(View, MotionEvent, android.view.ViewDebug.MotionEventTraceType) - * - * @hide - */ - public static void startMotionEventTracing(String prefix, View view) { - //noinspection PointlessBooleanExpression,ConstantConditions - if (!TRACE_MOTION_EVENTS) { - return; - } - - if (sMotionEventRoot != null) { - throw new IllegalStateException("You must call stopMotionEventTracing() before running" + - " a new trace!"); - } - - File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "motion-events/"); - //noinspection ResultOfMethodCallIgnored - hierarchyDump.mkdirs(); - - hierarchyDump = new File(hierarchyDump, prefix + ".traces"); - sMotionEventTracePrefix = prefix; - - try { - sMotionEventTraces = new BufferedWriter(new FileWriter(hierarchyDump), 32 * 1024); - } catch (IOException e) { - Log.e("View", "Could not dump view hierarchy"); - return; - } - - sMotionEventRoot = (ViewRoot) view.getRootView().getParent(); - } - - /** - * Stops the current motion events tracing. This method closes the file - * <code>/EXTERNAL/motion-events/PREFIX.traces</code>. - * - * Calling this method creates the file <code>/EXTERNAL/motion-events/PREFIX.tree</code> - * containing the view hierarchy of the view supplied to - * {@link #startMotionEventTracing(String, View)}. - * - * This method will return immediately if TRACE_HIERARCHY is false. - * - * @see #startMotionEventTracing(String, View) - * @see #trace(View, MotionEvent, android.view.ViewDebug.MotionEventTraceType) - * - * @hide - */ - public static void stopMotionEventTracing() { - //noinspection PointlessBooleanExpression,ConstantConditions - if (!TRACE_MOTION_EVENTS) { - return; - } - - if (sMotionEventRoot == null || sMotionEventTraces == null) { - throw new IllegalStateException("You must call startMotionEventTracing() before" + - " stopMotionEventTracing()!"); - } - - try { - sMotionEventTraces.close(); - } catch (IOException e) { - Log.e("View", "Could not write view traces"); - } - sMotionEventTraces = null; - - File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "motion-events/"); - //noinspection ResultOfMethodCallIgnored - hierarchyDump.mkdirs(); - hierarchyDump = new File(hierarchyDump, sMotionEventTracePrefix + ".tree"); - - BufferedWriter out; - try { - out = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024); - } catch (IOException e) { - Log.e("View", "Could not dump view hierarchy"); - return; - } - - View view = sMotionEventRoot.getView(); - if (view instanceof ViewGroup) { - ViewGroup group = (ViewGroup) view; - dumpViewHierarchy(group, out, 0); - try { - out.close(); - } catch (IOException e) { - Log.e("View", "Could not dump view hierarchy"); - } - } - - sHierarhcyRoot = null; - } - static void dispatchCommand(View view, String command, String parameters, OutputStream clientStream) throws IOException { @@ -870,6 +711,8 @@ public class ViewDebug { final String[] params = parameters.split(" "); if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) { capture(view, clientStream, params[0]); + } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) { + outputDisplayList(view, params[0]); } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) { invalidate(view, params[0]); } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) { @@ -1051,8 +894,10 @@ public class ViewDebug { try { T[] data = operation.pre(); long start = Debug.threadCpuTimeNanos(); + //noinspection unchecked operation.run(data); duration[0] = Debug.threadCpuTimeNanos() - start; + //noinspection unchecked operation.post(data); } finally { latch.countDown(); @@ -1141,6 +986,11 @@ public class ViewDebug { } } + private static void outputDisplayList(View root, String parameter) throws IOException { + final View view = findView(root, parameter); + view.getViewAncestor().outputDisplayList(view); + } + private static void capture(View root, final OutputStream clientStream, String parameter) throws IOException { @@ -1178,12 +1028,7 @@ public class ViewDebug { cache[0] = captureView.createSnapshot( Bitmap.Config.ARGB_8888, 0, skpiChildren); } catch (OutOfMemoryError e) { - try { - cache[0] = captureView.createSnapshot( - Bitmap.Config.ARGB_4444, 0, skpiChildren); - } catch (OutOfMemoryError e2) { - Log.w("View", "Out of memory for bitmap"); - } + Log.w("View", "Out of memory for bitmap"); } finally { latch.countDown(); } @@ -1293,7 +1138,6 @@ public class ViewDebug { } final HashMap<Class<?>, Field[]> map = sFieldsForClasses; - final HashMap<AccessibleObject, ExportedProperty> annotations = sAnnotations; Field[] fields = map.get(klass); if (fields != null) { @@ -1309,7 +1153,7 @@ public class ViewDebug { if (field.isAnnotationPresent(ExportedProperty.class)) { field.setAccessible(true); foundFields.add(field); - annotations.put(field, field.getAnnotation(ExportedProperty.class)); + sAnnotations.put(field, field.getAnnotation(ExportedProperty.class)); } } @@ -1328,7 +1172,6 @@ public class ViewDebug { } final HashMap<Class<?>, Method[]> map = sMethodsForClasses; - final HashMap<AccessibleObject, ExportedProperty> annotations = sAnnotations; Method[] methods = map.get(klass); if (methods != null) { @@ -1346,7 +1189,7 @@ public class ViewDebug { method.getReturnType() != Void.class) { method.setAccessible(true); foundMethods.add(method); - annotations.put(method, method.getAnnotation(ExportedProperty.class)); + sAnnotations.put(method, method.getAnnotation(ExportedProperty.class)); } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 8dc86ac..6937573 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -129,11 +129,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // First touch target in the linked list of touch targets. private TouchTarget mFirstTouchTarget; - // Temporary arrays for splitting pointers. - private int[] mTmpPointerIndexMap; - private int[] mTmpPointerIds; - private MotionEvent.PointerCoords[] mTmpPointerCoords; - // For debugging only. You can see these in hierarchyviewer. @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) @ViewDebug.ExportedProperty(category = "events") @@ -147,6 +142,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @ViewDebug.ExportedProperty(category = "events") private float mLastTouchDownY; + // Child which last received ACTION_HOVER_ENTER and ACTION_HOVER_MOVE. + private View mHoveredChild; + /** * Internal flags. * @@ -533,7 +531,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // note: knowing that mFocused is non-null is not a good enough reason // to break the traversal since in that case we'd actually have to find // the focused view and make sure it wasn't FOCUS_AFTER_DESCENDANTS and - // an ancestor of v; this will get checked for at ViewRoot + // an ancestor of v; this will get checked for at ViewAncestor && !(isFocused() && getDescendantFocusability() != FOCUS_AFTER_DESCENDANTS)) { mParent.focusableViewAvailable(v); } @@ -583,6 +581,36 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * {@inheritDoc} */ + public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) { + ViewParent parent = getParent(); + if (parent == null) { + return false; + } + final boolean propagate = onRequestSendAccessibilityEvent(child, event); + //noinspection SimplifiableIfStatement + if (!propagate) { + return false; + } + return parent.requestSendAccessibilityEvent(this, event); + } + + /** + * Called when a child has requested sending an {@link AccessibilityEvent} and + * gives an opportunity to its parent to augment the event. + * + * @param child The child which requests sending the event. + * @param event The event to be sent. + * @return True if the event should be sent. + * + * @see #requestSendAccessibilityEvent(View, AccessibilityEvent) + */ + public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { + return true; + } + + /** + * {@inheritDoc} + */ @Override public boolean dispatchUnhandledMove(View focused, int direction) { return mFocused != null && @@ -904,7 +932,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final float tx = event.mX; final float ty = event.mY; - ViewRoot root = getViewRoot(); + ViewAncestor root = getViewAncestor(); // Dispatch down the view hierarchy switch (event.mAction) { @@ -926,6 +954,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final View[] children = mChildren; for (int i = 0; i < count; i++) { final View child = children[i]; + child.mPrivateFlags2 &= ~View.DRAG_MASK; if (child.getVisibility() == VISIBLE) { final boolean handled = notifyChildOfDrag(children[i]); if (handled) { @@ -946,6 +975,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (View child : mDragNotifiedChildren) { // If a child was notified about an ongoing drag, it's told that it's over child.dispatchDragEvent(event); + child.mPrivateFlags2 &= ~View.DRAG_MASK; + child.refreshDrawableState(); } mDragNotifiedChildren.clear(); @@ -976,8 +1007,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int action = event.mAction; // If we've dragged off of a child view, send it the EXITED message if (mCurrentDragView != null) { + final View view = mCurrentDragView; event.mAction = DragEvent.ACTION_DRAG_EXITED; - mCurrentDragView.dispatchDragEvent(event); + view.dispatchDragEvent(event); + view.mPrivateFlags2 &= ~View.DRAG_HOVERED; + view.refreshDrawableState(); } mCurrentDragView = target; @@ -985,6 +1019,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (target != null) { event.mAction = DragEvent.ACTION_DRAG_ENTERED; target.dispatchDragEvent(event); + target.mPrivateFlags2 |= View.DRAG_HOVERED; + target.refreshDrawableState(); } event.mAction = action; // restore the event's original state } @@ -1015,7 +1051,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager case DragEvent.ACTION_DRAG_EXITED: { if (mCurrentDragView != null) { - mCurrentDragView.dispatchDragEvent(event); + final View view = mCurrentDragView; + view.dispatchDragEvent(event); + view.mPrivateFlags2 &= ~View.DRAG_HOVERED; + view.refreshDrawableState(); + mCurrentDragView = null; } } break; @@ -1053,7 +1093,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final View[] children = mChildren; for (int i = count - 1; i >= 0; i--) { final View child = children[i]; - if (!child.mCanAcceptDrop) { + if (!child.canAcceptDrag()) { continue; } @@ -1069,11 +1109,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager Log.d(View.VIEW_LOG_TAG, "Sending drag-started to view: " + child); } + boolean canAccept = false; if (! mDragNotifiedChildren.contains(child)) { mDragNotifiedChildren.add(child); - child.mCanAcceptDrop = child.dispatchDragEvent(mCurrentDrag); + canAccept = child.dispatchDragEvent(mCurrentDrag); + if (canAccept && !child.canAcceptDrag()) { + child.mPrivateFlags2 |= View.DRAG_CAN_ACCEPT; + child.refreshDrawableState(); + } } - return child.mCanAcceptDrop; + return canAccept; } @Override @@ -1106,10 +1151,22 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @Override public boolean dispatchKeyEvent(KeyEvent event) { + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onKeyEvent(event, 1); + } + if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { - return super.dispatchKeyEvent(event); + if (super.dispatchKeyEvent(event)) { + return true; + } } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) { - return mFocused.dispatchKeyEvent(event); + if (mFocused.dispatchKeyEvent(event)) { + return true; + } + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 1); } return false; } @@ -1132,21 +1189,70 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @Override public boolean dispatchTrackballEvent(MotionEvent event) { + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTrackballEvent(event, 1); + } + if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { - return super.dispatchTrackballEvent(event); + if (super.dispatchTrackballEvent(event)) { + return true; + } } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) { - return mFocused.dispatchTrackballEvent(event); + if (mFocused.dispatchTrackballEvent(event)) { + return true; + } + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 1); } return false; } - /** - * {@inheritDoc} - */ + /** @hide */ @Override - public boolean dispatchGenericMotionEvent(MotionEvent event) { - if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { - // Send the event to the child under the pointer. + protected boolean dispatchHoverEvent(MotionEvent event) { + // Send the hover enter or hover move event to the view group first. + // If it handles the event then a hovered child should receive hover exit. + boolean handled = false; + final boolean interceptHover; + final int action = event.getAction(); + if (action == MotionEvent.ACTION_HOVER_EXIT) { + interceptHover = true; + } else { + handled = super.dispatchHoverEvent(event); + interceptHover = handled; + } + + // Send successive hover events to the hovered child as long as the pointer + // remains within the child's bounds. + MotionEvent eventNoHistory = event; + if (mHoveredChild != null) { + final float x = event.getX(); + final float y = event.getY(); + + if (interceptHover + || !isTransformedTouchPointInView(x, y, mHoveredChild, null)) { + // Pointer exited the child. + // Send it a hover exit with only the most recent coordinates. We could + // try to find the exact point in history when the pointer left the view + // but it is not worth the effort. + eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory); + eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT); + handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, mHoveredChild); + eventNoHistory.setAction(action); + mHoveredChild = null; + } else { + // Pointer is still within the child. + //noinspection ConstantConditions + handled |= dispatchTransformedGenericPointerEvent(event, mHoveredChild); + } + } + + // Find a new hovered child if needed. + if (!interceptHover && mHoveredChild == null + && (action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_MOVE)) { final int childrenCount = mChildrenCount; if (childrenCount != 0) { final View[] children = mChildren; @@ -1155,45 +1261,100 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = childrenCount - 1; i >= 0; i--) { final View child = children[i]; - if ((child.mViewFlags & VISIBILITY_MASK) != VISIBLE - && child.getAnimation() == null) { - // Skip invisible child unless it is animating. + if (!canViewReceivePointerEvents(child) + || !isTransformedTouchPointInView(x, y, child, null)) { continue; } - if (!isTransformedTouchPointInView(x, y, child, null)) { - // Scroll point is out of child's bounds. - continue; + // Found the hovered child. + mHoveredChild = child; + if (action == MotionEvent.ACTION_HOVER_MOVE) { + // Pointer was moving within the view group and entered the child. + // Send it a hover enter and hover move with only the most recent + // coordinates. We could try to find the exact point in history when + // the pointer entered the view but it is not worth the effort. + eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory); + eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER); + handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child); + eventNoHistory.setAction(action); + + handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child); + } else { /* must be ACTION_HOVER_ENTER */ + // Pointer entered the child. + handled |= dispatchTransformedGenericPointerEvent(event, child); } + break; + } + } + } - final float offsetX = mScrollX - child.mLeft; - final float offsetY = mScrollY - child.mTop; - final boolean handled; - if (!child.hasIdentityMatrix()) { - MotionEvent transformedEvent = MotionEvent.obtain(event); - transformedEvent.offsetLocation(offsetX, offsetY); - transformedEvent.transform(child.getInverseMatrix()); - handled = child.dispatchGenericMotionEvent(transformedEvent); - transformedEvent.recycle(); - } else { - event.offsetLocation(offsetX, offsetY); - handled = child.dispatchGenericMotionEvent(event); - event.offsetLocation(-offsetX, -offsetY); - } + // Recycle the copy of the event that we made. + if (eventNoHistory != event) { + eventNoHistory.recycle(); + } - if (handled) { - return true; - } + // Send hover exit to the view group. If there was a child, we will already have + // sent the hover exit to it. + if (action == MotionEvent.ACTION_HOVER_EXIT) { + handled |= super.dispatchHoverEvent(event); + } + + // Done. + return handled; + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + // Handle the event only if leaf. This guarantees that + // the leafs (or any custom class that returns true from + // this method) will get a change to process the hover. + //noinspection SimplifiableIfStatement + if (getChildCount() == 0) { + return super.onHoverEvent(event); + } + return false; + } + + private static MotionEvent obtainMotionEventNoHistoryOrSelf(MotionEvent event) { + if (event.getHistorySize() == 0) { + return event; + } + return MotionEvent.obtainNoHistory(event); + } + + /** @hide */ + @Override + protected boolean dispatchGenericPointerEvent(MotionEvent event) { + // Send the event to the child under the pointer. + final int childrenCount = mChildrenCount; + if (childrenCount != 0) { + final View[] children = mChildren; + final float x = event.getX(); + final float y = event.getY(); + + for (int i = childrenCount - 1; i >= 0; i--) { + final View child = children[i]; + if (!canViewReceivePointerEvents(child) + || !isTransformedTouchPointInView(x, y, child, null)) { + continue; } - } - // No child handled the event. Send it to this view group. - return super.dispatchGenericMotionEvent(event); + if (dispatchTransformedGenericPointerEvent(event, child)) { + return true; + } + } } + // No child handled the event. Send it to this view group. + return super.dispatchGenericPointerEvent(event); + } + + /** @hide */ + @Override + protected boolean dispatchGenericFocusedEvent(MotionEvent event) { // Send the event to the focused child or to this view group if it has focus. if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { - return super.dispatchGenericMotionEvent(event); + return super.dispatchGenericFocusedEvent(event); } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) { return mFocused.dispatchGenericMotionEvent(event); } @@ -1201,166 +1362,193 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Dispatches a generic pointer event to a child, taking into account + * transformations that apply to the child. + * + * @param event The event to send. + * @param child The view to send the event to. + * @return {@code true} if the child handled the event. + */ + private boolean dispatchTransformedGenericPointerEvent(MotionEvent event, View child) { + final float offsetX = mScrollX - child.mLeft; + final float offsetY = mScrollY - child.mTop; + + boolean handled; + if (!child.hasIdentityMatrix()) { + MotionEvent transformedEvent = MotionEvent.obtain(event); + transformedEvent.offsetLocation(offsetX, offsetY); + transformedEvent.transform(child.getInverseMatrix()); + handled = child.dispatchGenericMotionEvent(transformedEvent); + transformedEvent.recycle(); + } else { + event.offsetLocation(offsetX, offsetY); + handled = child.dispatchGenericMotionEvent(event); + event.offsetLocation(-offsetX, -offsetY); + } + return handled; + } + + /** * {@inheritDoc} */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { - if (!onFilterTouchEventForSecurity(ev)) { - return false; - } - - final int action = ev.getAction(); - final int actionMasked = action & MotionEvent.ACTION_MASK; - - // Handle an initial down. - if (actionMasked == MotionEvent.ACTION_DOWN - || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { - // Throw away all previous state when starting a new touch gesture. - // The framework may have dropped the up or cancel event for the previous gesture - // due to an app switch, ANR, or some other state change. - cancelAndClearTouchTargets(ev); - resetTouchState(); + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } - // Check for interception. - final boolean intercepted; - if (actionMasked == MotionEvent.ACTION_DOWN - || mFirstTouchTarget != null) { - final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; - if (!disallowIntercept) { - intercepted = onInterceptTouchEvent(ev); - ev.setAction(action); // restore action in case onInterceptTouchEvent() changed it - } else { - intercepted = false; + boolean handled = false; + if (onFilterTouchEventForSecurity(ev)) { + final int action = ev.getAction(); + final int actionMasked = action & MotionEvent.ACTION_MASK; + + // Handle an initial down. + if (actionMasked == MotionEvent.ACTION_DOWN) { + // Throw away all previous state when starting a new touch gesture. + // The framework may have dropped the up or cancel event for the previous gesture + // due to an app switch, ANR, or some other state change. + cancelAndClearTouchTargets(ev); + resetTouchState(); } - } else { - // There are no touch targets and this action is not an initial down - // so this view group continues to intercept touches. - intercepted = true; - } - - // Check for cancelation. - final boolean canceled = resetCancelNextUpFlag(this) - || actionMasked == MotionEvent.ACTION_CANCEL; - // Update list of touch targets for pointer down, if needed. - final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; - TouchTarget newTouchTarget = null; - boolean alreadyDispatchedToNewTouchTarget = false; - if (!canceled && !intercepted) { + // Check for interception. + final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN - || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) - || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { - final int actionIndex = ev.getActionIndex(); // always 0 for down - final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) - : TouchTarget.ALL_POINTER_IDS; - - // Clean up earlier touch targets for this pointer id in case they - // have become out of sync. - removePointersFromTouchTargets(idBitsToAssign); - - final int childrenCount = mChildrenCount; - if (childrenCount != 0) { - // Find a child that can receive the event. Scan children from front to back. - final View[] children = mChildren; - final float x = ev.getX(actionIndex); - final float y = ev.getY(actionIndex); - - for (int i = childrenCount - 1; i >= 0; i--) { - final View child = children[i]; - if ((child.mViewFlags & VISIBILITY_MASK) != VISIBLE - && child.getAnimation() == null) { - // Skip invisible child unless it is animating. - continue; - } + || mFirstTouchTarget != null) { + final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; + if (!disallowIntercept) { + intercepted = onInterceptTouchEvent(ev); + ev.setAction(action); // restore action in case it was changed + } else { + intercepted = false; + } + } else { + // There are no touch targets and this action is not an initial down + // so this view group continues to intercept touches. + intercepted = true; + } - if (!isTransformedTouchPointInView(x, y, child, null)) { - // New pointer is out of child's bounds. - continue; - } + // Check for cancelation. + final boolean canceled = resetCancelNextUpFlag(this) + || actionMasked == MotionEvent.ACTION_CANCEL; + + // Update list of touch targets for pointer down, if needed. + final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; + TouchTarget newTouchTarget = null; + boolean alreadyDispatchedToNewTouchTarget = false; + if (!canceled && !intercepted) { + if (actionMasked == MotionEvent.ACTION_DOWN + || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) + || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { + final int actionIndex = ev.getActionIndex(); // always 0 for down + final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) + : TouchTarget.ALL_POINTER_IDS; + + // Clean up earlier touch targets for this pointer id in case they + // have become out of sync. + removePointersFromTouchTargets(idBitsToAssign); + + final int childrenCount = mChildrenCount; + if (childrenCount != 0) { + // Find a child that can receive the event. + // Scan children from front to back. + final View[] children = mChildren; + final float x = ev.getX(actionIndex); + final float y = ev.getY(actionIndex); + + for (int i = childrenCount - 1; i >= 0; i--) { + final View child = children[i]; + if (!canViewReceivePointerEvents(child) + || !isTransformedTouchPointInView(x, y, child, null)) { + continue; + } - newTouchTarget = getTouchTarget(child); - if (newTouchTarget != null) { - // Child is already receiving touch within its bounds. - // Give it the new pointer in addition to the ones it is handling. - newTouchTarget.pointerIdBits |= idBitsToAssign; - break; - } + newTouchTarget = getTouchTarget(child); + if (newTouchTarget != null) { + // Child is already receiving touch within its bounds. + // Give it the new pointer in addition to the ones it is handling. + newTouchTarget.pointerIdBits |= idBitsToAssign; + break; + } - resetCancelNextUpFlag(child); - if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { - // Child wants to receive touch within its bounds. - mLastTouchDownTime = ev.getDownTime(); - mLastTouchDownIndex = i; - mLastTouchDownX = ev.getX(); - mLastTouchDownY = ev.getY(); - newTouchTarget = addTouchTarget(child, idBitsToAssign); - alreadyDispatchedToNewTouchTarget = true; - break; + resetCancelNextUpFlag(child); + if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { + // Child wants to receive touch within its bounds. + mLastTouchDownTime = ev.getDownTime(); + mLastTouchDownIndex = i; + mLastTouchDownX = ev.getX(); + mLastTouchDownY = ev.getY(); + newTouchTarget = addTouchTarget(child, idBitsToAssign); + alreadyDispatchedToNewTouchTarget = true; + break; + } } } - } - if (newTouchTarget == null && mFirstTouchTarget != null) { - // Did not find a child to receive the event. - // Assign the pointer to the least recently added target. - newTouchTarget = mFirstTouchTarget; - while (newTouchTarget.next != null) { - newTouchTarget = newTouchTarget.next; + if (newTouchTarget == null && mFirstTouchTarget != null) { + // Did not find a child to receive the event. + // Assign the pointer to the least recently added target. + newTouchTarget = mFirstTouchTarget; + while (newTouchTarget.next != null) { + newTouchTarget = newTouchTarget.next; + } + newTouchTarget.pointerIdBits |= idBitsToAssign; } - newTouchTarget.pointerIdBits |= idBitsToAssign; } } - } - // Dispatch to touch targets. - boolean handled = false; - if (mFirstTouchTarget == null) { - // No touch targets so treat this as an ordinary view. - handled = dispatchTransformedTouchEvent(ev, canceled, null, - TouchTarget.ALL_POINTER_IDS); - } else { - // Dispatch to touch targets, excluding the new touch target if we already - // dispatched to it. Cancel touch targets if necessary. - TouchTarget predecessor = null; - TouchTarget target = mFirstTouchTarget; - while (target != null) { - final TouchTarget next = target.next; - if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { - handled = true; - } else { - final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; - if (dispatchTransformedTouchEvent(ev, cancelChild, - target.child, target.pointerIdBits)) { + // Dispatch to touch targets. + if (mFirstTouchTarget == null) { + // No touch targets so treat this as an ordinary view. + handled = dispatchTransformedTouchEvent(ev, canceled, null, + TouchTarget.ALL_POINTER_IDS); + } else { + // Dispatch to touch targets, excluding the new touch target if we already + // dispatched to it. Cancel touch targets if necessary. + TouchTarget predecessor = null; + TouchTarget target = mFirstTouchTarget; + while (target != null) { + final TouchTarget next = target.next; + if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; - } - if (cancelChild) { - if (predecessor == null) { - mFirstTouchTarget = next; - } else { - predecessor.next = next; + } else { + final boolean cancelChild = resetCancelNextUpFlag(target.child) + || intercepted; + if (dispatchTransformedTouchEvent(ev, cancelChild, + target.child, target.pointerIdBits)) { + handled = true; + } + if (cancelChild) { + if (predecessor == null) { + mFirstTouchTarget = next; + } else { + predecessor.next = next; + } + target.recycle(); + target = next; + continue; } - target.recycle(); - target = next; - continue; } + predecessor = target; + target = next; } - predecessor = target; - target = next; } - } - // Update list of touch targets for pointer up or cancel, if needed. - if (canceled - || actionMasked == MotionEvent.ACTION_UP - || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { - resetTouchState(); - } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { - final int actionIndex = ev.getActionIndex(); - final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); - removePointersFromTouchTargets(idBitsToRemove); + // Update list of touch targets for pointer up or cancel, if needed. + if (canceled + || actionMasked == MotionEvent.ACTION_UP + || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { + resetTouchState(); + } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { + final int actionIndex = ev.getActionIndex(); + final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); + removePointersFromTouchTargets(idBitsToRemove); + } } + if (!handled && mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); + } return handled; } @@ -1476,6 +1664,15 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Returns true if a child view can receive pointer events. + * @hide + */ + private static boolean canViewReceivePointerEvents(View child) { + return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE + || child.getAnimation() != null; + } + + /** * Returns true if a child view contains the specified point when transformed * into its coordinate space. * Child must not be null. @@ -1524,141 +1721,38 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } // Calculate the number of pointers to deliver. - final int oldPointerCount = event.getPointerCount(); - int newPointerCount = 0; - if (desiredPointerIdBits == TouchTarget.ALL_POINTER_IDS) { - newPointerCount = oldPointerCount; - } else { - for (int i = 0; i < oldPointerCount; i++) { - final int pointerId = event.getPointerId(i); - final int pointerIdBit = 1 << pointerId; - if ((pointerIdBit & desiredPointerIdBits) != 0) { - newPointerCount += 1; - } - } - } + final int oldPointerIdBits = event.getPointerIdBits(); + final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; // If for some reason we ended up in an inconsistent state where it looks like we // might produce a motion event with no pointers in it, then drop the event. - if (newPointerCount == 0) { + if (newPointerIdBits == 0) { return false; } // If the number of pointers is the same and we don't need to perform any fancy // irreversible transformations, then we can reuse the motion event for this // dispatch as long as we are careful to revert any changes we make. - final boolean reuse = newPointerCount == oldPointerCount - && (child == null || child.hasIdentityMatrix()); - if (reuse) { - if (child == null) { - handled = super.dispatchTouchEvent(event); - } else { - final float offsetX = mScrollX - child.mLeft; - final float offsetY = mScrollY - child.mTop; - event.offsetLocation(offsetX, offsetY); - - handled = child.dispatchTouchEvent(event); - - event.offsetLocation(-offsetX, -offsetY); - } - return handled; - } - - // Make a copy of the event. - // If the number of pointers is different, then we need to filter out irrelevant pointers - // as we make a copy of the motion event. - MotionEvent transformedEvent; - if (newPointerCount == oldPointerCount) { - transformedEvent = MotionEvent.obtain(event); - } else { - growTmpPointerArrays(newPointerCount); - final int[] newPointerIndexMap = mTmpPointerIndexMap; - final int[] newPointerIds = mTmpPointerIds; - final MotionEvent.PointerCoords[] newPointerCoords = mTmpPointerCoords; - - int newPointerIndex = 0; - int oldPointerIndex = 0; - while (newPointerIndex < newPointerCount) { - final int pointerId = event.getPointerId(oldPointerIndex); - final int pointerIdBits = 1 << pointerId; - if ((pointerIdBits & desiredPointerIdBits) != 0) { - newPointerIndexMap[newPointerIndex] = oldPointerIndex; - newPointerIds[newPointerIndex] = pointerId; - if (newPointerCoords[newPointerIndex] == null) { - newPointerCoords[newPointerIndex] = new MotionEvent.PointerCoords(); - } - - newPointerIndex += 1; - } - oldPointerIndex += 1; - } - - final int newAction; - if (cancel) { - newAction = MotionEvent.ACTION_CANCEL; - } else { - final int oldMaskedAction = oldAction & MotionEvent.ACTION_MASK; - if (oldMaskedAction == MotionEvent.ACTION_POINTER_DOWN - || oldMaskedAction == MotionEvent.ACTION_POINTER_UP) { - final int changedPointerId = event.getPointerId( - (oldAction & MotionEvent.ACTION_POINTER_INDEX_MASK) - >> MotionEvent.ACTION_POINTER_INDEX_SHIFT); - final int changedPointerIdBits = 1 << changedPointerId; - if ((changedPointerIdBits & desiredPointerIdBits) != 0) { - if (newPointerCount == 1) { - // The first/last pointer went down/up. - newAction = oldMaskedAction == MotionEvent.ACTION_POINTER_DOWN - ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP; - } else { - // A secondary pointer went down/up. - int newChangedPointerIndex = 0; - while (newPointerIds[newChangedPointerIndex] != changedPointerId) { - newChangedPointerIndex += 1; - } - newAction = oldMaskedAction | (newChangedPointerIndex - << MotionEvent.ACTION_POINTER_INDEX_SHIFT); - } - } else { - // An unrelated pointer changed. - newAction = MotionEvent.ACTION_MOVE; - } + // Otherwise we need to make a copy. + final MotionEvent transformedEvent; + if (newPointerIdBits == oldPointerIdBits) { + if (child == null || child.hasIdentityMatrix()) { + if (child == null) { + handled = super.dispatchTouchEvent(event); } else { - // Simple up/down/cancel/move motion action. - newAction = oldMaskedAction; - } - } + final float offsetX = mScrollX - child.mLeft; + final float offsetY = mScrollY - child.mTop; + event.offsetLocation(offsetX, offsetY); - transformedEvent = null; - final int historySize = event.getHistorySize(); - for (int historyIndex = 0; historyIndex <= historySize; historyIndex++) { - for (newPointerIndex = 0; newPointerIndex < newPointerCount; newPointerIndex++) { - final MotionEvent.PointerCoords c = newPointerCoords[newPointerIndex]; - oldPointerIndex = newPointerIndexMap[newPointerIndex]; - if (historyIndex != historySize) { - event.getHistoricalPointerCoords(oldPointerIndex, historyIndex, c); - } else { - event.getPointerCoords(oldPointerIndex, c); - } - } + handled = child.dispatchTouchEvent(event); - final long eventTime; - if (historyIndex != historySize) { - eventTime = event.getHistoricalEventTime(historyIndex); - } else { - eventTime = event.getEventTime(); - } - - if (transformedEvent == null) { - transformedEvent = MotionEvent.obtain( - event.getDownTime(), eventTime, newAction, - newPointerCount, newPointerIds, newPointerCoords, - event.getMetaState(), event.getXPrecision(), event.getYPrecision(), - event.getDeviceId(), event.getEdgeFlags(), event.getSource(), - event.getFlags()); - } else { - transformedEvent.addBatch(eventTime, newPointerCoords, 0); + event.offsetLocation(-offsetX, -offsetY); } + return handled; } + transformedEvent = MotionEvent.obtain(event); + } else { + transformedEvent = event.split(newPointerIdBits); } // Perform any necessary transformations and dispatch. @@ -1681,36 +1775,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Enlarge the temporary pointer arrays for splitting pointers. - * May discard contents (but keeps PointerCoords objects to avoid reallocating them). - */ - private void growTmpPointerArrays(int desiredCapacity) { - final MotionEvent.PointerCoords[] oldTmpPointerCoords = mTmpPointerCoords; - int capacity; - if (oldTmpPointerCoords != null) { - capacity = oldTmpPointerCoords.length; - if (desiredCapacity <= capacity) { - return; - } - } else { - capacity = 4; - } - - while (capacity < desiredCapacity) { - capacity *= 2; - } - - mTmpPointerIndexMap = new int[capacity]; - mTmpPointerIds = new int[capacity]; - mTmpPointerCoords = new MotionEvent.PointerCoords[capacity]; - - if (oldTmpPointerCoords != null) { - System.arraycopy(oldTmpPointerCoords, 0, mTmpPointerCoords, 0, - oldTmpPointerCoords.length); - } - } - - /** * Enable or disable the splitting of MotionEvents to multiple children during touch event * dispatch. This behavior is enabled by default for applications that target an * SDK version of {@link Build.VERSION_CODES#HONEYCOMB} or newer. @@ -1818,7 +1882,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @see #FOCUS_BEFORE_DESCENDANTS * @see #FOCUS_AFTER_DESCENDANTS * @see #FOCUS_BLOCK_DESCENDANTS - * @see #onRequestFocusInDescendants + * @see #onRequestFocusInDescendants(int, android.graphics.Rect) */ @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { @@ -1931,11 +1995,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = false; + // We first get a chance to populate the event. + onPopulateAccessibilityEvent(event); + // Let our children have a shot in populating the event. for (int i = 0, count = getChildCount(); i < count; i++) { - populated |= getChildAt(i).dispatchPopulateAccessibilityEvent(event); + boolean handled = getChildAt(i).dispatchPopulateAccessibilityEvent(event); + if (handled) { + return handled; + } } - return populated; + return false; } /** @@ -1975,7 +2044,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager public void setPadding(int left, int top, int right, int bottom) { super.setPadding(left, top, right, bottom); - if ((mPaddingLeft | mPaddingTop | mPaddingRight | mPaddingRight) != 0) { + if ((mPaddingLeft | mPaddingTop | mPaddingRight | mPaddingBottom) != 0) { mGroupFlags |= FLAG_PADDING_NOT_NULL; } else { mGroupFlags &= ~FLAG_PADDING_NOT_NULL; @@ -1999,10 +2068,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Perform dispatching of a {@link #saveHierarchyState freeze()} to only this view, - * not to its children. For use when overriding - * {@link #dispatchSaveInstanceState dispatchFreeze()} to allow subclasses to freeze - * their own state but not the state of their children. + * Perform dispatching of a {@link #saveHierarchyState(android.util.SparseArray)} freeze()} + * to only this view, not to its children. For use when overriding + * {@link #dispatchSaveInstanceState(android.util.SparseArray)} dispatchFreeze()} to allow + * subclasses to freeze their own state but not the state of their children. * * @param container the container */ @@ -2027,10 +2096,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Perform dispatching of a {@link #restoreHierarchyState thaw()} to only this view, - * not to its children. For use when overriding - * {@link #dispatchRestoreInstanceState dispatchThaw()} to allow subclasses to thaw - * their own state but not the state of their children. + * Perform dispatching of a {@link #restoreHierarchyState(android.util.SparseArray)} + * to only this view, not to its children. For use when overriding + * {@link #dispatchRestoreInstanceState(android.util.SparseArray)} to allow + * subclasses to thaw their own state but not the state of their children. * * @param container the container */ @@ -3244,6 +3313,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mTransition.removeChild(this, view); } + if (view == mHoveredChild) { + mHoveredChild = null; + } + boolean clearChildFocus = false; if (view == mFocused) { view.clearFocusForRemoval(); @@ -3307,6 +3380,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final OnHierarchyChangeListener onHierarchyChangeListener = mOnHierarchyChangeListener; final boolean notifyListener = onHierarchyChangeListener != null; final View focused = mFocused; + final View hoveredChild = mHoveredChild; final boolean detach = mAttachInfo != null; View clearChildFocus = null; @@ -3320,6 +3394,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mTransition.removeChild(this, view); } + if (view == hoveredChild) { + mHoveredChild = null; + } + if (view == focused) { view.clearFocusForRemoval(); clearChildFocus = view; @@ -3377,6 +3455,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final OnHierarchyChangeListener listener = mOnHierarchyChangeListener; final boolean notify = listener != null; final View focused = mFocused; + final View hoveredChild = mHoveredChild; final boolean detach = mAttachInfo != null; View clearChildFocus = null; @@ -3389,6 +3468,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mTransition.removeChild(this, view); } + if (view == hoveredChild) { + mHoveredChild = null; + } + if (view == focused) { view.clearFocusForRemoval(); clearChildFocus = view; @@ -3610,13 +3693,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (drawAnimation) { if (view != null) { view.mPrivateFlags |= DRAW_ANIMATION; - } else if (parent instanceof ViewRoot) { - ((ViewRoot) parent).mIsAnimating = true; + } else if (parent instanceof ViewAncestor) { + ((ViewAncestor) parent).mIsAnimating = true; } } - if (parent instanceof ViewRoot) { - ((ViewRoot) parent).invalidate(); + if (parent instanceof ViewAncestor) { + ((ViewAncestor) parent).invalidate(); parent = null; } else if (view != null) { if ((view.mPrivateFlags & DRAWN) == DRAWN || @@ -3671,8 +3754,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (drawAnimation) { if (view != null) { view.mPrivateFlags |= DRAW_ANIMATION; - } else if (parent instanceof ViewRoot) { - ((ViewRoot) parent).mIsAnimating = true; + } else if (parent instanceof ViewAncestor) { + ((ViewAncestor) parent).mIsAnimating = true; } } @@ -4195,7 +4278,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // If this group is dirty, check that the parent is dirty as well if ((mPrivateFlags & DIRTY_MASK) != 0) { final ViewParent parent = getParent(); - if (parent != null && !(parent instanceof ViewRoot)) { + if (parent != null && !(parent instanceof ViewAncestor)) { if ((((View) parent).mPrivateFlags & DIRTY_MASK) == 0) { result = false; android.util.Log.d(ViewDebug.CONSISTENCY_LOG_TAG, @@ -4755,6 +4838,19 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Return true if the pressed state should be delayed for children or descendants of this + * ViewGroup. Generally, this should be done for containers that can scroll, such as a List. + * This prevents the pressed state from appearing when the user is actually trying to scroll + * the content. + * + * The default implementation returns true for compatibility reasons. Subclasses that do + * not scroll should generally override this method and return false. + */ + public boolean shouldDelayChildPressedState() { + return true; + } + + /** * LayoutParams are used by views to tell their parents how they want to be * laid out. See * {@link android.R.styleable#ViewGroup_Layout ViewGroup Layout Attributes} diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index d7d4c3f..655df39 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -17,6 +17,7 @@ package android.view; import android.graphics.Rect; +import android.view.accessibility.AccessibilityEvent; /** * Defines the responsibilities for a class that will be a parent of a View. @@ -222,4 +223,22 @@ public interface ViewParent { */ public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate); + + /** + * Called by a child to request from its parent to send an {@link AccessibilityEvent}. + * The child has already populated a record for itself in the event and is delegating + * to its parent to send the event. The parent can optionally add a record for itself. + * <p> + * Note: An accessibility event is fired by an individual view which populates the + * event with a record for its state and requests from its parent to perform + * the sending. The parent can optionally add a record for itself before + * dispatching the request to its parent. A parent can also choose not to + * respect the request for sending the event. The accessibility event is sent + * by the topmost view in the view tree. + * + * @param child The child which requests sending the event. + * @param event The event to be sent. + * @return True if the event was sent. + */ + public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event); } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 8a18aaf..9395d5c 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -388,6 +388,12 @@ public interface WindowManager extends ViewManager { public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18; /** + * Window type: Navigation bar (when distinct from status bar) + * @hide + */ + public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19; + + /** * End of types of system windows. */ public static final int LAST_SYSTEM_WINDOW = 2999; diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index a4c4544..d7a3096 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -19,7 +19,6 @@ package android.view; import android.graphics.PixelFormat; import android.os.IBinder; import android.util.AndroidRuntimeException; -import android.util.Config; import android.util.Log; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; @@ -102,7 +101,7 @@ public class WindowManagerImpl implements WindowManager { private void addView(View view, ViewGroup.LayoutParams params, boolean nest) { - if (Config.LOGV) Log.v("WindowManager", "addView view=" + view); + if (false) Log.v("WindowManager", "addView view=" + view); if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException( @@ -112,7 +111,7 @@ public class WindowManagerImpl implements WindowManager { final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; - ViewRoot root; + ViewAncestor root; View panelParentView = null; synchronized (this) { @@ -149,7 +148,7 @@ public class WindowManagerImpl implements WindowManager { } } - root = new ViewRoot(view.getContext()); + root = new ViewAncestor(view.getContext()); root.mAddNesting = 1; view.setLayoutParams(wparams); @@ -157,7 +156,7 @@ public class WindowManagerImpl implements WindowManager { if (mViews == null) { index = 1; mViews = new View[1]; - mRoots = new ViewRoot[1]; + mRoots = new ViewAncestor[1]; mParams = new WindowManager.LayoutParams[1]; } else { index = mViews.length + 1; @@ -165,7 +164,7 @@ public class WindowManagerImpl implements WindowManager { mViews = new View[index]; System.arraycopy(old, 0, mViews, 0, index-1); old = mRoots; - mRoots = new ViewRoot[index]; + mRoots = new ViewAncestor[index]; System.arraycopy(old, 0, mRoots, 0, index-1); old = mParams; mParams = new WindowManager.LayoutParams[index]; @@ -193,7 +192,7 @@ public class WindowManagerImpl implements WindowManager { synchronized (this) { int index = findViewLocked(view, true); - ViewRoot root = mRoots[index]; + ViewAncestor root = mRoots[index]; mParams[index] = wparams; root.setLayoutParams(wparams, false); } @@ -208,14 +207,14 @@ public class WindowManagerImpl implements WindowManager { } throw new IllegalStateException("Calling with view " + view - + " but the ViewRoot is attached to " + curView); + + " but the ViewAncestor is attached to " + curView); } } public void removeViewImmediate(View view) { synchronized (this) { int index = findViewLocked(view, true); - ViewRoot root = mRoots[index]; + ViewAncestor root = mRoots[index]; View curView = root.getView(); root.mAddNesting = 0; @@ -226,12 +225,12 @@ public class WindowManagerImpl implements WindowManager { } throw new IllegalStateException("Calling with view " + view - + " but the ViewRoot is attached to " + curView); + + " but the ViewAncestor is attached to " + curView); } } View removeViewLocked(int index) { - ViewRoot root = mRoots[index]; + ViewAncestor root = mRoots[index]; View view = root.getView(); // Don't really remove until we have matched all calls to add(). @@ -257,7 +256,7 @@ public class WindowManagerImpl implements WindowManager { removeItem(tmpViews, mViews, index); mViews = tmpViews; - ViewRoot[] tmpRoots = new ViewRoot[count-1]; + ViewAncestor[] tmpRoots = new ViewAncestor[count-1]; removeItem(tmpRoots, mRoots, index); mRoots = tmpRoots; @@ -282,7 +281,7 @@ public class WindowManagerImpl implements WindowManager { //Log.i("foo", "@ " + i + " token " + mParams[i].token // + " view " + mRoots[i].getView()); if (token == null || mParams[i].token == token) { - ViewRoot root = mRoots[i]; + ViewAncestor root = mRoots[i]; root.mAddNesting = 1; //Log.i("foo", "Force closing " + root); @@ -309,7 +308,7 @@ public class WindowManagerImpl implements WindowManager { int count = mViews.length; for (int i=0; i<count; i++) { if (token == null || mParams[i].token == token) { - ViewRoot root = mRoots[i]; + ViewAncestor root = mRoots[i]; root.setStopped(stopped); } } @@ -318,13 +317,13 @@ public class WindowManagerImpl implements WindowManager { public WindowManager.LayoutParams getRootViewLayoutParameter(View view) { ViewParent vp = view.getParent(); - while (vp != null && !(vp instanceof ViewRoot)) { + while (vp != null && !(vp instanceof ViewAncestor)) { vp = vp.getParent(); } if (vp == null) return null; - ViewRoot vr = (ViewRoot)vp; + ViewAncestor vr = (ViewAncestor)vp; int N = mRoots.length; for (int i = 0; i < N; ++i) { @@ -345,7 +344,7 @@ public class WindowManagerImpl implements WindowManager { } private View[] mViews; - private ViewRoot[] mRoots; + private ViewAncestor[] mRoots; private WindowManager.LayoutParams[] mParams; private static void removeItem(Object[] dst, Object[] src, int index) diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 086ed5a..4e52e40 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -81,6 +81,8 @@ public interface WindowManagerPolicy { public final static int FLAG_INJECTED = 0x01000000; public final static int FLAG_TRUSTED = 0x02000000; + public final static int FLAG_FILTERED = 0x04000000; + public final static int FLAG_DISABLE_KEY_REPEAT = 0x08000000; public final static int FLAG_WOKE_HERE = 0x10000000; public final static int FLAG_BRIGHT_HERE = 0x20000000; diff --git a/core/java/android/view/WindowOrientationListener.java b/core/java/android/view/WindowOrientationListener.java index 62d3e6a..d128b57 100755 --- a/core/java/android/view/WindowOrientationListener.java +++ b/core/java/android/view/WindowOrientationListener.java @@ -21,7 +21,6 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; -import android.util.Config; import android.util.Log; import android.util.Slog; @@ -47,7 +46,7 @@ import android.util.Slog; public abstract class WindowOrientationListener { private static final String TAG = "WindowOrientationListener"; private static final boolean DEBUG = false; - private static final boolean localLOGV = DEBUG || Config.DEBUG; + private static final boolean localLOGV = DEBUG || false; private SensorManager mSensorManager; private boolean mEnabled; diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index fc61700..5e18f55 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -21,13 +21,26 @@ import android.os.Parcelable; import android.text.TextUtils; import java.util.ArrayList; -import java.util.List; /** * This class represents accessibility events that are sent by the system when * something notable happens in the user interface. For example, when a * {@link android.widget.Button} is clicked, a {@link android.view.View} is focused, etc. * <p> + * An accessibility event is fired by an individual view which populates the event with + * a record for its state and requests from its parent to send the event to interested + * parties. The parent can optionally add a record for itself before dispatching a similar + * request to its parent. A parent can also choose not to respect the request for sending + * an event. The accessibility event is sent by the topmost view in the view tree. + * Therefore, an {@link android.accessibilityservice.AccessibilityService} can explore + * all records in an accessibility event to obtain more information about the context + * in which the event was fired. + * <p> + * A client can add, remove, and modify records. The getters and setters for individual + * properties operate on the current record which can be explicitly set by the client. By + * default current is the first record. Thus, querying a record would require setting + * it as the current one and interacting with the property getters and setters. + * <p> * This class represents various semantically different accessibility event * types. Each event type has associated a set of related properties. In other * words, each event type is characterized via a subset of the properties exposed @@ -145,7 +158,7 @@ import java.util.List; * @see android.view.accessibility.AccessibilityManager * @see android.accessibilityservice.AccessibilityService */ -public final class AccessibilityEvent implements Parcelable { +public final class AccessibilityEvent extends AccessibilityRecord implements Parcelable { /** * Invalid selection/focus position. @@ -159,7 +172,12 @@ public final class AccessibilityEvent implements Parcelable { * * @see #getBeforeText() * @see #getText() + * </br> + * Note: This constant is no longer needed since there + * is no limit on the length of text that is contained + * in an accessibility event anymore. */ + @Deprecated public static final int MAX_TEXT_LENGTH = 500; /** @@ -202,6 +220,26 @@ public final class AccessibilityEvent implements Parcelable { public static final int TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040; /** + * Represents the event of a hover enter over a {@link android.view.View}. + */ + public static final int TYPE_VIEW_HOVER_ENTER = 0x00000080; + + /** + * Represents the event of a hover exit over a {@link android.view.View}. + */ + public static final int TYPE_VIEW_HOVER_EXIT = 0x00000100; + + /** + * Represents the event of starting a touch exploration gesture. + */ + public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200; + + /** + * Represents the event of ending a touch exploration gesture. + */ + public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400; + + /** * Mask for {@link AccessibilityEvent} all types. * * @see #TYPE_VIEW_CLICKED @@ -214,116 +252,53 @@ public final class AccessibilityEvent implements Parcelable { */ public static final int TYPES_ALL_MASK = 0xFFFFFFFF; - private static final int MAX_POOL_SIZE = 2; - private static final Object mPoolLock = new Object(); + private static final int MAX_POOL_SIZE = 10; + private static final Object sPoolLock = new Object(); private static AccessibilityEvent sPool; private static int sPoolSize; - private static final int CHECKED = 0x00000001; - private static final int ENABLED = 0x00000002; - private static final int PASSWORD = 0x00000004; - private static final int FULL_SCREEN = 0x00000080; - private AccessibilityEvent mNext; + private boolean mIsInPool; private int mEventType; - private int mBooleanProperties; - private int mCurrentItemIndex; - private int mItemCount; - private int mFromIndex; - private int mAddedCount; - private int mRemovedCount; - - private long mEventTime; - - private CharSequence mClassName; private CharSequence mPackageName; - private CharSequence mContentDescription; - private CharSequence mBeforeText; - - private Parcelable mParcelableData; - - private final List<CharSequence> mText = new ArrayList<CharSequence>(); + private long mEventTime; - private boolean mIsInPool; + private final ArrayList<AccessibilityRecord> mRecords = new ArrayList<AccessibilityRecord>(); /* * Hide constructor from clients. */ private AccessibilityEvent() { - mCurrentItemIndex = INVALID_POSITION; - } - - /** - * Gets if the source is checked. - * - * @return True if the view is checked, false otherwise. - */ - public boolean isChecked() { - return getBooleanProperty(CHECKED); - } - - /** - * Sets if the source is checked. - * - * @param isChecked True if the view is checked, false otherwise. - */ - public void setChecked(boolean isChecked) { - setBooleanProperty(CHECKED, isChecked); - } - /** - * Gets if the source is enabled. - * - * @return True if the view is enabled, false otherwise. - */ - public boolean isEnabled() { - return getBooleanProperty(ENABLED); - } - - /** - * Sets if the source is enabled. - * - * @param isEnabled True if the view is enabled, false otherwise. - */ - public void setEnabled(boolean isEnabled) { - setBooleanProperty(ENABLED, isEnabled); - } - - /** - * Gets if the source is a password field. - * - * @return True if the view is a password field, false otherwise. - */ - public boolean isPassword() { - return getBooleanProperty(PASSWORD); } /** - * Sets if the source is a password field. + * Gets the number of records contained in the event. * - * @param isPassword True if the view is a password field, false otherwise. + * @return The number of records. */ - public void setPassword(boolean isPassword) { - setBooleanProperty(PASSWORD, isPassword); + public int getRecordCount() { + return mRecords.size(); } /** - * Sets if the source is taking the entire screen. + * Appends an {@link AccessibilityRecord} to the end of event records. * - * @param isFullScreen True if the source is full screen, false otherwise. + * @param record The record to append. */ - public void setFullScreen(boolean isFullScreen) { - setBooleanProperty(FULL_SCREEN, isFullScreen); + public void appendRecord(AccessibilityRecord record) { + mRecords.add(record); } /** - * Gets if the source is taking the entire screen. + * Gets the records at a given index. * - * @return True if the source is full screen, false otherwise. + * @param index The index. + * @return The records at the specified index. */ - public boolean isFullScreen() { - return getBooleanProperty(FULL_SCREEN); + public AccessibilityRecord getRecord(int index) { + return mRecords.get(index); } /** @@ -345,96 +320,6 @@ public final class AccessibilityEvent implements Parcelable { } /** - * Gets the number of items that can be visited. - * - * @return The number of items. - */ - public int getItemCount() { - return mItemCount; - } - - /** - * Sets the number of items that can be visited. - * - * @param itemCount The number of items. - */ - public void setItemCount(int itemCount) { - mItemCount = itemCount; - } - - /** - * Gets the index of the source in the list of items the can be visited. - * - * @return The current item index. - */ - public int getCurrentItemIndex() { - return mCurrentItemIndex; - } - - /** - * Sets the index of the source in the list of items that can be visited. - * - * @param currentItemIndex The current item index. - */ - public void setCurrentItemIndex(int currentItemIndex) { - mCurrentItemIndex = currentItemIndex; - } - - /** - * Gets the index of the first character of the changed sequence. - * - * @return The index of the first character. - */ - public int getFromIndex() { - return mFromIndex; - } - - /** - * Sets the index of the first character of the changed sequence. - * - * @param fromIndex The index of the first character. - */ - public void setFromIndex(int fromIndex) { - mFromIndex = fromIndex; - } - - /** - * Gets the number of added characters. - * - * @return The number of added characters. - */ - public int getAddedCount() { - return mAddedCount; - } - - /** - * Sets the number of added characters. - * - * @param addedCount The number of added characters. - */ - public void setAddedCount(int addedCount) { - mAddedCount = addedCount; - } - - /** - * Gets the number of removed characters. - * - * @return The number of removed characters. - */ - public int getRemovedCount() { - return mRemovedCount; - } - - /** - * Sets the number of removed characters. - * - * @param removedCount The number of removed characters. - */ - public void setRemovedCount(int removedCount) { - mRemovedCount = removedCount; - } - - /** * Gets the time in which this event was sent. * * @return The event time. @@ -453,24 +338,6 @@ public final class AccessibilityEvent implements Parcelable { } /** - * Gets the class name of the source. - * - * @return The class name. - */ - public CharSequence getClassName() { - return mClassName; - } - - /** - * Sets the class name of the source. - * - * @param className The lass name. - */ - public void setClassName(CharSequence className) { - mClassName = className; - } - - /** * Gets the package name of the source. * * @return The package name. @@ -489,70 +356,6 @@ public final class AccessibilityEvent implements Parcelable { } /** - * Gets the text of the event. The index in the list represents the priority - * of the text. Specifically, the lower the index the higher the priority. - * - * @return The text. - */ - public List<CharSequence> getText() { - return mText; - } - - /** - * Sets the text before a change. - * - * @return The text before the change. - */ - public CharSequence getBeforeText() { - return mBeforeText; - } - - /** - * Sets the text before a change. - * - * @param beforeText The text before the change. - */ - public void setBeforeText(CharSequence beforeText) { - mBeforeText = beforeText; - } - - /** - * Gets the description of the source. - * - * @return The description. - */ - public CharSequence getContentDescription() { - return mContentDescription; - } - - /** - * Sets the description of the source. - * - * @param contentDescription The description. - */ - public void setContentDescription(CharSequence contentDescription) { - mContentDescription = contentDescription; - } - - /** - * Gets the {@link Parcelable} data. - * - * @return The parcelable data. - */ - public Parcelable getParcelableData() { - return mParcelableData; - } - - /** - * Sets the {@link Parcelable} data of the event. - * - * @param parcelableData The parcelable data. - */ - public void setParcelableData(Parcelable parcelableData) { - mParcelableData = parcelableData; - } - - /** * Returns a cached instance if such is available or a new one is * instantiated with type property set. * @@ -572,7 +375,7 @@ public final class AccessibilityEvent implements Parcelable { * @return An instance. */ public static AccessibilityEvent obtain() { - synchronized (mPoolLock) { + synchronized (sPoolLock) { if (sPool != null) { AccessibilityEvent event = sPool; sPool = sPool.mNext; @@ -589,14 +392,16 @@ public final class AccessibilityEvent implements Parcelable { * Return an instance back to be reused. * <p> * <b>Note: You must not touch the object after calling this function.</b> + * + * @throws IllegalStateException If the event is already recycled. */ + @Override public void recycle() { if (mIsInPool) { - return; + throw new IllegalStateException("Event already recycled!"); } - clear(); - synchronized (mPoolLock) { + synchronized (sPoolLock) { if (sPoolSize <= MAX_POOL_SIZE) { mNext = sPool; sPool = this; @@ -609,44 +414,15 @@ public final class AccessibilityEvent implements Parcelable { /** * Clears the state of this instance. */ - private void clear() { + @Override + protected void clear() { + super.clear(); mEventType = 0; - mBooleanProperties = 0; - mCurrentItemIndex = INVALID_POSITION; - mItemCount = 0; - mFromIndex = 0; - mAddedCount = 0; - mRemovedCount = 0; - mEventTime = 0; - mClassName = null; mPackageName = null; - mContentDescription = null; - mBeforeText = null; - mParcelableData = null; - mText.clear(); - } - - /** - * Gets the value of a boolean property. - * - * @param property The property. - * @return The value. - */ - private boolean getBooleanProperty(int property) { - return (mBooleanProperties & property) == property; - } - - /** - * Sets a boolean property. - * - * @param property The property. - * @param value The value. - */ - private void setBooleanProperty(int property, boolean value) { - if (value) { - mBooleanProperties |= property; - } else { - mBooleanProperties &= ~property; + mEventTime = 0; + while (!mRecords.isEmpty()) { + AccessibilityRecord record = mRecords.remove(0); + record.recycle(); } } @@ -657,38 +433,82 @@ public final class AccessibilityEvent implements Parcelable { */ public void initFromParcel(Parcel parcel) { mEventType = parcel.readInt(); - mBooleanProperties = parcel.readInt(); - mCurrentItemIndex = parcel.readInt(); - mItemCount = parcel.readInt(); - mFromIndex = parcel.readInt(); - mAddedCount = parcel.readInt(); - mRemovedCount = parcel.readInt(); - mEventTime = parcel.readLong(); - mClassName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); - mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); - mBeforeText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); - mParcelableData = parcel.readParcelable(null); - parcel.readList(mText, null); + mEventTime = parcel.readLong(); + readAccessibilityRecordFromParcel(this, parcel); + + // Read the records. + final int recordCount = parcel.readInt(); + for (int i = 0; i < recordCount; i++) { + AccessibilityRecord record = AccessibilityRecord.obtain(); + readAccessibilityRecordFromParcel(record, parcel); + mRecords.add(record); + } } + /** + * Reads an {@link AccessibilityRecord} from a parcel. + * + * @param record The record to initialize. + * @param parcel The parcel to read from. + */ + private void readAccessibilityRecordFromParcel(AccessibilityRecord record, + Parcel parcel) { + record.mBooleanProperties = parcel.readInt(); + record.mCurrentItemIndex = parcel.readInt(); + record.mItemCount = parcel.readInt(); + record.mFromIndex = parcel.readInt(); + record.mAddedCount = parcel.readInt(); + record.mRemovedCount = parcel.readInt(); + record.mClassName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + record.mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + record.mBeforeText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + record.mParcelableData = parcel.readParcelable(null); + parcel.readList(record.mText, null); + } + + /** + * {@inheritDoc} + */ public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(mEventType); - parcel.writeInt(mBooleanProperties); - parcel.writeInt(mCurrentItemIndex); - parcel.writeInt(mItemCount); - parcel.writeInt(mFromIndex); - parcel.writeInt(mAddedCount); - parcel.writeInt(mRemovedCount); - parcel.writeLong(mEventTime); - TextUtils.writeToParcel(mClassName, parcel, 0); TextUtils.writeToParcel(mPackageName, parcel, 0); - TextUtils.writeToParcel(mContentDescription, parcel, 0); - TextUtils.writeToParcel(mBeforeText, parcel, 0); - parcel.writeParcelable(mParcelableData, flags); - parcel.writeList(mText); + parcel.writeLong(mEventTime); + writeAccessibilityRecordToParcel(this, parcel, flags); + + // Write the records. + final int recordCount = getRecordCount(); + parcel.writeInt(recordCount); + for (int i = 0; i < recordCount; i++) { + AccessibilityRecord record = mRecords.get(i); + writeAccessibilityRecordToParcel(record, parcel, flags); + } + } + + /** + * Writes an {@link AccessibilityRecord} to a parcel. + * + * @param record The record to write. + * @param parcel The parcel to which to write. + */ + private void writeAccessibilityRecordToParcel(AccessibilityRecord record, Parcel parcel, + int flags) { + parcel.writeInt(record.mBooleanProperties); + parcel.writeInt(record.mCurrentItemIndex); + parcel.writeInt(record.mItemCount); + parcel.writeInt(record.mFromIndex); + parcel.writeInt(record.mAddedCount); + parcel.writeInt(record.mRemovedCount); + TextUtils.writeToParcel(record.mClassName, parcel, flags); + TextUtils.writeToParcel(record.mContentDescription, parcel, flags); + TextUtils.writeToParcel(record.mBeforeText, parcel, flags); + parcel.writeParcelable(record.mParcelableData, flags); + parcel.writeList(record.mText); } + /** + * {@inheritDoc} + */ public int describeContents() { return 0; } @@ -696,24 +516,21 @@ public final class AccessibilityEvent implements Parcelable { @Override public String toString() { StringBuilder builder = new StringBuilder(); - builder.append(super.toString()); builder.append("; EventType: " + mEventType); builder.append("; EventTime: " + mEventTime); - builder.append("; ClassName: " + mClassName); builder.append("; PackageName: " + mPackageName); - builder.append("; Text: " + mText); - builder.append("; ContentDescription: " + mContentDescription); - builder.append("; ItemCount: " + mItemCount); - builder.append("; CurrentItemIndex: " + mCurrentItemIndex); - builder.append("; IsEnabled: " + isEnabled()); - builder.append("; IsPassword: " + isPassword()); - builder.append("; IsChecked: " + isChecked()); - builder.append("; IsFullScreen: " + isFullScreen()); - builder.append("; BeforeText: " + mBeforeText); - builder.append("; FromIndex: " + mFromIndex); - builder.append("; AddedCount: " + mAddedCount); - builder.append("; RemovedCount: " + mRemovedCount); - builder.append("; ParcelableData: " + mParcelableData); + builder.append(" \n{\n"); + builder.append(super.toString()); + builder.append("\n"); + for (int i = 0; i < mRecords.size(); i++) { + AccessibilityRecord record = mRecords.get(i); + builder.append(" Record "); + builder.append(i); + builder.append(":"); + builder.append(record.toString()); + builder.append("\n"); + } + builder.append("}\n"); return builder.toString(); } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index f406da9..dd77193 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -16,8 +16,8 @@ package android.view.accessibility; -import static android.util.Config.LOGV; - +import android.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; @@ -46,6 +46,8 @@ import java.util.List; * @see android.content.Context#getSystemService */ public final class AccessibilityManager { + private static final boolean DEBUG = false; + private static final String LOG_TAG = "AccessibilityManager"; static final Object sInstanceSync = new Object(); @@ -166,7 +168,7 @@ public final class AccessibilityManager { long identityToken = Binder.clearCallingIdentity(); doRecycle = mService.sendAccessibilityEvent(event); Binder.restoreCallingIdentity(identityToken); - if (LOGV) { + if (DEBUG) { Log.i(LOG_TAG, event + " sent"); } } catch (RemoteException re) { @@ -187,7 +189,7 @@ public final class AccessibilityManager { } try { mService.interrupt(); - if (LOGV) { + if (DEBUG) { Log.i(LOG_TAG, "Requested interrupt from all services"); } } catch (RemoteException re) { @@ -204,7 +206,33 @@ public final class AccessibilityManager { List<ServiceInfo> services = null; try { services = mService.getAccessibilityServiceList(); - if (LOGV) { + if (DEBUG) { + Log.i(LOG_TAG, "Installed AccessibilityServices " + services); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while obtaining the installed AccessibilityServices. ", re); + } + return Collections.unmodifiableList(services); + } + + /** + * Returns the {@link ServiceInfo}s of the enabled accessibility services + * for a given feedback type. + * + * @param feedbackType The type of feedback. + * @return An unmodifiable list with {@link ServiceInfo}s. + * + * @see AccessibilityServiceInfo#FEEDBACK_AUDIBLE + * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC + * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN + * @see AccessibilityServiceInfo#FEEDBACK_VISUAL + * @see AccessibilityServiceInfo#FEEDBACK_GENERIC + */ + public List<ServiceInfo> getEnabledAccessibilityServiceList(int feedbackType) { + List<ServiceInfo> services = null; + try { + services = mService.getEnabledAccessibilityServiceList(feedbackType); + if (DEBUG) { Log.i(LOG_TAG, "Installed AccessibilityServices " + services); } } catch (RemoteException re) { diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java new file mode 100644 index 0000000..7819b17 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.accessibility; + +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a record in an accessibility event. This class encapsulates + * the information for a {@link android.view.View}. Note that not all properties + * are applicable to all view types. For detailed information please refer to + * {@link AccessibilityEvent}. + * + * @see AccessibilityEvent + */ +public class AccessibilityRecord { + + private static final int INVALID_POSITION = -1; + + private static final int PROPERTY_CHECKED = 0x00000001; + private static final int PROPERTY_ENABLED = 0x00000002; + private static final int PROPERTY_PASSWORD = 0x00000004; + private static final int PROPERTY_FULL_SCREEN = 0x00000080; + + private static final int MAX_POOL_SIZE = 10; + private static final Object sPoolLock = new Object(); + private static AccessibilityRecord sPool; + private static int sPoolSize; + + private AccessibilityRecord mNext; + private boolean mIsInPool; + + protected int mBooleanProperties; + protected int mCurrentItemIndex; + protected int mItemCount; + protected int mFromIndex; + protected int mAddedCount; + protected int mRemovedCount; + + protected CharSequence mClassName; + protected CharSequence mContentDescription; + protected CharSequence mBeforeText; + protected Parcelable mParcelableData; + + protected final List<CharSequence> mText = new ArrayList<CharSequence>(); + + /* + * Hide constructor. + */ + protected AccessibilityRecord() { + + } + + /** + * Gets if the source is checked. + * + * @return True if the view is checked, false otherwise. + */ + public boolean isChecked() { + return getBooleanProperty(PROPERTY_CHECKED); + } + + /** + * Sets if the source is checked. + * + * @param isChecked True if the view is checked, false otherwise. + */ + public void setChecked(boolean isChecked) { + setBooleanProperty(PROPERTY_CHECKED, isChecked); + } + + /** + * Gets if the source is enabled. + * + * @return True if the view is enabled, false otherwise. + */ + public boolean isEnabled() { + return getBooleanProperty(PROPERTY_ENABLED); + } + + /** + * Sets if the source is enabled. + * + * @param isEnabled True if the view is enabled, false otherwise. + */ + public void setEnabled(boolean isEnabled) { + setBooleanProperty(PROPERTY_ENABLED, isEnabled); + } + + /** + * Gets if the source is a password field. + * + * @return True if the view is a password field, false otherwise. + */ + public boolean isPassword() { + return getBooleanProperty(PROPERTY_PASSWORD); + } + + /** + * Sets if the source is a password field. + * + * @param isPassword True if the view is a password field, false otherwise. + */ + public void setPassword(boolean isPassword) { + setBooleanProperty(PROPERTY_PASSWORD, isPassword); + } + + /** + * Sets if the source is taking the entire screen. + * + * @param isFullScreen True if the source is full screen, false otherwise. + */ + public void setFullScreen(boolean isFullScreen) { + setBooleanProperty(PROPERTY_FULL_SCREEN, isFullScreen); + } + + /** + * Gets if the source is taking the entire screen. + * + * @return True if the source is full screen, false otherwise. + */ + public boolean isFullScreen() { + return getBooleanProperty(PROPERTY_FULL_SCREEN); + } + + /** + * Gets the number of items that can be visited. + * + * @return The number of items. + */ + public int getItemCount() { + return mItemCount; + } + + /** + * Sets the number of items that can be visited. + * + * @param itemCount The number of items. + */ + public void setItemCount(int itemCount) { + mItemCount = itemCount; + } + + /** + * Gets the index of the source in the list of items the can be visited. + * + * @return The current item index. + */ + public int getCurrentItemIndex() { + return mCurrentItemIndex; + } + + /** + * Sets the index of the source in the list of items that can be visited. + * + * @param currentItemIndex The current item index. + */ + public void setCurrentItemIndex(int currentItemIndex) { + mCurrentItemIndex = currentItemIndex; + } + + /** + * Gets the index of the first character of the changed sequence. + * + * @return The index of the first character. + */ + public int getFromIndex() { + return mFromIndex; + } + + /** + * Sets the index of the first character of the changed sequence. + * + * @param fromIndex The index of the first character. + */ + public void setFromIndex(int fromIndex) { + mFromIndex = fromIndex; + } + + /** + * Gets the number of added characters. + * + * @return The number of added characters. + */ + public int getAddedCount() { + return mAddedCount; + } + + /** + * Sets the number of added characters. + * + * @param addedCount The number of added characters. + */ + public void setAddedCount(int addedCount) { + mAddedCount = addedCount; + } + + /** + * Gets the number of removed characters. + * + * @return The number of removed characters. + */ + public int getRemovedCount() { + return mRemovedCount; + } + + /** + * Sets the number of removed characters. + * + * @param removedCount The number of removed characters. + */ + public void setRemovedCount(int removedCount) { + mRemovedCount = removedCount; + } + + /** + * Gets the class name of the source. + * + * @return The class name. + */ + public CharSequence getClassName() { + return mClassName; + } + + /** + * Sets the class name of the source. + * + * @param className The lass name. + */ + public void setClassName(CharSequence className) { + mClassName = className; + } + + /** + * Gets the text of the event. The index in the list represents the priority + * of the text. Specifically, the lower the index the higher the priority. + * + * @return The text. + */ + public List<CharSequence> getText() { + return mText; + } + + /** + * Sets the text before a change. + * + * @return The text before the change. + */ + public CharSequence getBeforeText() { + return mBeforeText; + } + + /** + * Sets the text before a change. + * + * @param beforeText The text before the change. + */ + public void setBeforeText(CharSequence beforeText) { + mBeforeText = beforeText; + } + + /** + * Gets the description of the source. + * + * @return The description. + */ + public CharSequence getContentDescription() { + return mContentDescription; + } + + /** + * Sets the description of the source. + * + * @param contentDescription The description. + */ + public void setContentDescription(CharSequence contentDescription) { + mContentDescription = contentDescription; + } + + /** + * Gets the {@link Parcelable} data. + * + * @return The parcelable data. + */ + public Parcelable getParcelableData() { + return mParcelableData; + } + + /** + * Sets the {@link Parcelable} data of the event. + * + * @param parcelableData The parcelable data. + */ + public void setParcelableData(Parcelable parcelableData) { + mParcelableData = parcelableData; + } + + /** + * Gets the value of a boolean property. + * + * @param property The property. + * @return The value. + */ + public boolean getBooleanProperty(int property) { + return (mBooleanProperties & property) == property; + } + + /** + * Sets a boolean property. + * + * @param property The property. + * @param value The value. + */ + private void setBooleanProperty(int property, boolean value) { + if (value) { + mBooleanProperties |= property; + } else { + mBooleanProperties &= ~property; + } + } + + /** + * Returns a cached instance if such is available or a new one is + * instantiated. + * + * @return An instance. + */ + public static AccessibilityRecord obtain() { + synchronized (sPoolLock) { + if (sPool != null) { + AccessibilityRecord record = sPool; + sPool = sPool.mNext; + sPoolSize--; + record.mNext = null; + record.mIsInPool = false; + return record; + } + return new AccessibilityRecord(); + } + } + + /** + * Return an instance back to be reused. + * <p> + * <b>Note: You must not touch the object after calling this function.</b> + * + * @throws IllegalStateException If the record is already recycled. + */ + public void recycle() { + if (mIsInPool) { + throw new IllegalStateException("Record already recycled!"); + } + clear(); + synchronized (sPoolLock) { + if (sPoolSize <= MAX_POOL_SIZE) { + mNext = sPool; + sPool = this; + mIsInPool = true; + sPoolSize++; + } + } + } + + /** + * Clears the state of this instance. + */ + protected void clear() { + mBooleanProperties = 0; + mCurrentItemIndex = INVALID_POSITION; + mItemCount = 0; + mFromIndex = 0; + mAddedCount = 0; + mRemovedCount = 0; + mClassName = null; + mContentDescription = null; + mBeforeText = null; + mParcelableData = null; + mText.clear(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(" [ ClassName: " + mClassName); + builder.append("; Text: " + mText); + builder.append("; ContentDescription: " + mContentDescription); + builder.append("; ItemCount: " + mItemCount); + builder.append("; CurrentItemIndex: " + mCurrentItemIndex); + builder.append("; IsEnabled: " + getBooleanProperty(PROPERTY_ENABLED)); + builder.append("; IsPassword: " + getBooleanProperty(PROPERTY_PASSWORD)); + builder.append("; IsChecked: " + getBooleanProperty(PROPERTY_CHECKED)); + builder.append("; IsFullScreen: " + getBooleanProperty(PROPERTY_FULL_SCREEN)); + builder.append("; BeforeText: " + mBeforeText); + builder.append("; FromIndex: " + mFromIndex); + builder.append("; AddedCount: " + mAddedCount); + builder.append("; RemovedCount: " + mRemovedCount); + builder.append("; ParcelableData: " + mParcelableData); + builder.append(" ]"); + return builder.toString(); + } +} diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 7633569..aaaae32 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -35,5 +35,7 @@ interface IAccessibilityManager { List<ServiceInfo> getAccessibilityServiceList(); + List<ServiceInfo> getEnabledAccessibilityServiceList(int feedbackType); + void interrupt(); } diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index e644045..b4303f4 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -34,7 +34,7 @@ import android.util.LogPrinter; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; -import android.view.ViewRoot; +import android.view.ViewAncestor; class ComposingText implements NoCopySpan { } @@ -501,7 +501,7 @@ public class BaseInputConnection implements InputConnection { } } if (h != null) { - h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, event)); } } @@ -644,7 +644,7 @@ public class BaseInputConnection implements InputConnection { lp.println("Composing text:"); TextUtils.dumpSpans(text, lp, " "); } - + // Position the cursor appropriately, so that after replacing the // desired range of text it will be located in the correct spot. // This allows us to deal with filters performing edits on the text diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index 4d9d51e..690ea85 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -58,8 +58,7 @@ public class InputConnectionWrapper implements InputConnection { return mTarget.getCursorCapsMode(reqModes); } - public ExtractedText getExtractedText(ExtractedTextRequest request, - int flags) { + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { return mTarget.getExtractedText(request, flags); } diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index 32eec9f..1f7441d 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -322,7 +322,12 @@ public final class InputMethodInfo implements Parcelable { InputMethodInfo obj = (InputMethodInfo) o; return mId.equals(obj.mId); } - + + @Override + public int hashCode() { + return mId.hashCode(); + } + /** * Used to package this object into a {@link Parcel}. * diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index a39c7c7..27cbaf7 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -41,7 +41,7 @@ import android.util.Printer; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; -import android.view.ViewRoot; +import android.view.ViewAncestor; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -505,6 +505,13 @@ public final class InputMethodManager { } } + /** + * Returns a list of enabled input method subtypes for the specified input method info. + * @param imi An input method info whose subtypes list will be returned. + * @param allowsImplicitlySelectedSubtypes A boolean flag to allow to return the implicitly + * selected subtypes. If an input method info doesn't have enabled subtypes, the framework + * will implicitly enable subtypes according to the current system language. + */ public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) { try { @@ -629,7 +636,7 @@ public final class InputMethodManager { if (vh != null) { // This will result in a call to reportFinishInputConnection() // below. - vh.sendMessage(vh.obtainMessage(ViewRoot.FINISH_INPUT_CONNECTION, + vh.sendMessage(vh.obtainMessage(ViewAncestor.FINISH_INPUT_CONNECTION, mServedInputConnection)); } } @@ -1086,9 +1093,9 @@ public final class InputMethodManager { void scheduleCheckFocusLocked(View view) { Handler vh = view.getHandler(); - if (vh != null && !vh.hasMessages(ViewRoot.CHECK_FOCUS)) { + if (vh != null && !vh.hasMessages(ViewAncestor.CHECK_FOCUS)) { // This will result in a call to checkFocus() below. - vh.sendMessage(vh.obtainMessage(ViewRoot.CHECK_FOCUS)); + vh.sendMessage(vh.obtainMessage(ViewAncestor.CHECK_FOCUS)); } } @@ -1143,7 +1150,7 @@ public final class InputMethodManager { } /** - * Called by ViewRoot when its window gets input focus. + * Called by ViewAncestor when its window gets input focus. * @hide */ public void onWindowFocus(View rootView, View focusedView, int softInputMode, @@ -1429,16 +1436,26 @@ public final class InputMethodManager { } } - public void showInputMethodAndSubtypeEnabler(String topId) { + /** + * Show the settings for enabling subtypes of the specified input method. + * @param imiId An input method, whose subtypes settings will be shown. If imiId is null, + * subtypes of all input methods will be shown. + */ + public void showInputMethodAndSubtypeEnabler(String imiId) { synchronized (mH) { try { - mService.showInputMethodAndSubtypeEnablerFromClient(mClient, topId); + mService.showInputMethodAndSubtypeEnablerFromClient(mClient, imiId); } catch (RemoteException e) { Log.w(TAG, "IME died: " + mCurId, e); } } } + /** + * Returns the current input method subtype. This subtype is one of the subtypes in + * the current input method. This method returns null when the current input method doesn't + * have any input method subtype. + */ public InputMethodSubtype getCurrentInputMethodSubtype() { synchronized (mH) { try { @@ -1450,6 +1467,12 @@ public final class InputMethodManager { } } + /** + * Switch to a new input method subtype of the current input method. + * @param subtype A new input method subtype to switch. + * @return true if the current subtype was successfully switched. When the specified subtype is + * null, this method returns false. + */ public boolean setCurrentInputMethodSubtype(InputMethodSubtype subtype) { synchronized (mH) { try { @@ -1461,6 +1484,9 @@ public final class InputMethodManager { } } + /** + * Returns a map of all shortcut input method info and their subtypes. + */ public Map<InputMethodInfo, List<InputMethodSubtype>> getShortcutInputMethodsAndSubtypes() { synchronized (mH) { HashMap<InputMethodInfo, List<InputMethodSubtype>> ret = @@ -1493,6 +1519,15 @@ public final class InputMethodManager { } } + /** + * Force switch to the last used input method and subtype. If the last input method didn't have + * any subtypes, the framework will simply switch to the last input method with no subtype + * specified. + * @param imeToken Supplies the identifying token given to an input method when it was started, + * which allows it to perform this operation on itself. + * @return true if the current input method and subtype was successfully switched to the last + * used input method and subtype. + */ public boolean switchToLastInputMethod(IBinder imeToken) { synchronized (mH) { try { @@ -1504,6 +1539,17 @@ public final class InputMethodManager { } } + public InputMethodSubtype getLastInputMethodSubtype() { + synchronized (mH) { + try { + return mService.getLastInputMethodSubtype(); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + return null; + } + } + } + void doDump(FileDescriptor fd, PrintWriter fout, String[] args) { final Printer p = new PrintWriterPrinter(fout); p.println("Input method client state for " + this + ":"); diff --git a/core/java/android/view/inputmethod/InputMethodSubtype.java b/core/java/android/view/inputmethod/InputMethodSubtype.java index 807f6ce..de38fbe 100644 --- a/core/java/android/view/inputmethod/InputMethodSubtype.java +++ b/core/java/android/view/inputmethod/InputMethodSubtype.java @@ -38,12 +38,13 @@ public final class InputMethodSubtype implements Parcelable { private static final String EXTRA_VALUE_PAIR_SEPARATOR = ","; private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "="; - private final int mSubtypeNameResId; + private final boolean mIsAuxiliary; + private final int mSubtypeHashCode; private final int mSubtypeIconResId; + private final int mSubtypeNameResId; private final String mSubtypeLocale; private final String mSubtypeMode; private final String mSubtypeExtraValue; - private final int mSubtypeHashCode; private HashMap<String, String> mExtraValueHashMapCache; /** @@ -55,12 +56,27 @@ public final class InputMethodSubtype implements Parcelable { * @param extraValue The extra value of the subtype */ InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue) { + this(nameId, iconId, locale, mode, extraValue, false); + } + + /** + * Constructor + * @param nameId The name of the subtype + * @param iconId The icon of the subtype + * @param locale The locale supported by the subtype + * @param modeId The mode supported by the subtype + * @param extraValue The extra value of the subtype + * @param isAuxiliary true when this subtype is one shot subtype. + */ + InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue, + boolean isAuxiliary) { mSubtypeNameResId = nameId; mSubtypeIconResId = iconId; mSubtypeLocale = locale != null ? locale : ""; mSubtypeMode = mode != null ? mode : ""; mSubtypeExtraValue = extraValue != null ? extraValue : ""; mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue); + mIsAuxiliary = isAuxiliary; } InputMethodSubtype(Parcel source) { @@ -74,6 +90,7 @@ public final class InputMethodSubtype implements Parcelable { s = source.readString(); mSubtypeExtraValue = s != null ? s : ""; mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue); + mIsAuxiliary = (source.readInt() == 1); } /** @@ -111,6 +128,15 @@ public final class InputMethodSubtype implements Parcelable { return mSubtypeExtraValue; } + /** + * @return true if this subtype is one shot subtype. One shot subtype will not be shown in the + * ime switch list when this subtype is implicitly enabled. The framework will never + * switch the current ime to this subtype by switchToLastInputMethod in InputMethodManager. + */ + public boolean isAuxiliary() { + return mIsAuxiliary; + } + private HashMap<String, String> getExtraValueHashMap() { if (mExtraValueHashMapCache == null) { mExtraValueHashMapCache = new HashMap<String, String>(); @@ -170,24 +196,29 @@ public final class InputMethodSubtype implements Parcelable { return false; } + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel dest, int parcelableFlags) { dest.writeInt(mSubtypeNameResId); dest.writeInt(mSubtypeIconResId); dest.writeString(mSubtypeLocale); dest.writeString(mSubtypeMode); dest.writeString(mSubtypeExtraValue); + dest.writeInt(mIsAuxiliary ? 1 : 0); } public static final Parcelable.Creator<InputMethodSubtype> CREATOR = new Parcelable.Creator<InputMethodSubtype>() { + @Override public InputMethodSubtype createFromParcel(Parcel source) { return new InputMethodSubtype(source); } + @Override public InputMethodSubtype[] newArray(int size) { return new InputMethodSubtype[size]; } diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index c7a7374..9f2fd12 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -35,7 +35,7 @@ import android.os.Message; import android.util.Log; import android.util.TypedValue; import android.view.Surface; -import android.view.ViewRoot; +import android.view.ViewAncestor; import android.view.WindowManager; import junit.framework.Assert; @@ -222,7 +222,7 @@ class BrowserFrame extends Handler { sConfigCallback = new ConfigCallback( (WindowManager) appContext.getSystemService( Context.WINDOW_SERVICE)); - ViewRoot.addConfigCallback(sConfigCallback); + ViewAncestor.addConfigCallback(sConfigCallback); } sConfigCallback.addHandler(this); @@ -1043,13 +1043,16 @@ class BrowserFrame extends Handler { // These ids need to be in sync with enum rawResId in PlatformBridge.h private static final int NODOMAIN = 1; private static final int LOADERROR = 2; - private static final int DRAWABLEDIR = 3; + /* package */ static final int DRAWABLEDIR = 3; private static final int FILE_UPLOAD_LABEL = 4; private static final int RESET_LABEL = 5; private static final int SUBMIT_LABEL = 6; private static final int FILE_UPLOAD_NO_FILE_CHOSEN = 7; - String getRawResFilename(int id) { + private String getRawResFilename(int id) { + return getRawResFilename(id, mContext); + } + /* package */ static String getRawResFilename(int id, Context context) { int resid; switch (id) { case NODOMAIN: @@ -1066,19 +1069,19 @@ class BrowserFrame extends Handler { break; case FILE_UPLOAD_LABEL: - return mContext.getResources().getString( + return context.getResources().getString( com.android.internal.R.string.upload_file); case RESET_LABEL: - return mContext.getResources().getString( + return context.getResources().getString( com.android.internal.R.string.reset); case SUBMIT_LABEL: - return mContext.getResources().getString( + return context.getResources().getString( com.android.internal.R.string.submit); case FILE_UPLOAD_NO_FILE_CHOSEN: - return mContext.getResources().getString( + return context.getResources().getString( com.android.internal.R.string.no_file_chosen); default: @@ -1086,7 +1089,7 @@ class BrowserFrame extends Handler { return ""; } TypedValue value = new TypedValue(); - mContext.getResources().getValue(resid, value, true); + context.getResources().getValue(resid, value, true); if (id == DRAWABLEDIR) { String path = value.string.toString(); int index = path.lastIndexOf('/'); diff --git a/core/java/android/webkit/HTML5VideoFullScreen.java b/core/java/android/webkit/HTML5VideoFullScreen.java index 0918683..1004b5f 100644 --- a/core/java/android/webkit/HTML5VideoFullScreen.java +++ b/core/java/android/webkit/HTML5VideoFullScreen.java @@ -208,7 +208,7 @@ public class HTML5VideoFullScreen extends HTML5VideoView // view. This happens in the WebChromeClient before this method // is invoked. pauseAndDispatch(mProxy); - + mProxy.dispatchOnStopFullScreen(); mLayout.removeView(getSurfaceView()); if (mProgressView != null) { @@ -253,7 +253,8 @@ public class HTML5VideoFullScreen extends HTML5VideoView client.onShowCustomView(mLayout, mCallback); // Plugins like Flash will draw over the video so hide // them while we're playing. - mProxy.getWebView().getViewManager().hideAll(); + if (webView.getViewManager() != null) + webView.getViewManager().hideAll(); mProgressView = client.getVideoLoadingProgressView(); if (mProgressView != null) { diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java index d1b8cfc..7d8669b 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -65,6 +65,7 @@ class HTML5VideoViewProxy extends Handler private static final int ENDED = 201; private static final int POSTER_FETCHED = 202; private static final int PAUSED = 203; + private static final int STOPFULLSCREEN = 204; // Timer thread -> UI thread private static final int TIMEUPDATE = 300; @@ -287,8 +288,13 @@ class HTML5VideoViewProxy extends Handler } public void dispatchOnPaused() { - Message msg = Message.obtain(mWebCoreHandler, PAUSED); - mWebCoreHandler.sendMessage(msg); + Message msg = Message.obtain(mWebCoreHandler, PAUSED); + mWebCoreHandler.sendMessage(msg); + } + + public void dispatchOnStopFullScreen() { + Message msg = Message.obtain(mWebCoreHandler, STOPFULLSCREEN); + mWebCoreHandler.sendMessage(msg); } public void onTimeupdate() { @@ -560,6 +566,9 @@ class HTML5VideoViewProxy extends Handler case TIMEUPDATE: nativeOnTimeupdate(msg.arg1, mNativePointer); break; + case STOPFULLSCREEN: + nativeOnStopFullscreen(mNativePointer); + break; } } }; @@ -686,6 +695,7 @@ class HTML5VideoViewProxy extends Handler private native void nativeOnPaused(int nativePointer); private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); private native void nativeOnTimeupdate(int position, int nativePointer); + private native void nativeOnStopFullscreen(int nativePointer); private native static boolean nativeSendSurfaceTexture(SurfaceTexture texture, int baseLayer, int videoLayerId, int textureName, int playerState); diff --git a/core/java/android/webkit/JniUtil.java b/core/java/android/webkit/JniUtil.java index 62b415c..b5d4933 100644 --- a/core/java/android/webkit/JniUtil.java +++ b/core/java/android/webkit/JniUtil.java @@ -48,6 +48,12 @@ class JniUtil { initialized = true; } + protected static synchronized Context getContext() { + if (!initialized) + return null; + return sContext; + } + /** * Called by JNI. Gets the application's database directory, excluding the trailing slash. * @return String The application's database directory diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 2b507fd..8ffbda2 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -19,7 +19,6 @@ package android.webkit; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.content.res.Resources; import android.os.Build; import android.os.Handler; import android.os.Message; @@ -139,6 +138,9 @@ public class WebSettings { OFF } + // TODO: Keep this up to date + private static final String PREVIOUS_VERSION = "3.1"; + // WebView associated with this WebSettings. private WebView mWebView; // BrowserFrame used to access the native frame pointer. @@ -221,6 +223,7 @@ public class WebSettings { private boolean mAllowContentAccess = true; private boolean mLoadWithOverviewMode = false; private boolean mEnableSmoothTransition = false; + private boolean mForceUserScalable = false; // AutoFill Profile data /** @@ -471,7 +474,14 @@ public class WebSettings { // Add version final String version = Build.VERSION.RELEASE; if (version.length() > 0) { - buffer.append(version); + if (Character.isDigit(version.charAt(0))) { + // Release is a version, eg "3.1" + buffer.append(version); + } else { + // Release is a codename, eg "Honeycomb" + // In this case, use the previous release's version + buffer.append(PREVIOUS_VERSION); + } } else { // default to "1.0" buffer.append("1.0"); @@ -1672,6 +1682,23 @@ public class WebSettings { } } + /** + * Returns whether the viewport metatag can disable zooming + * @hide + */ + public boolean forceUserScalable() { + return mForceUserScalable; + } + + /** + * Sets whether viewport metatag can disable zooming. + * @param flag Whether or not to forceably enable user scalable. + * @hide + */ + public synchronized void setForceUserScalable(boolean flag) { + mForceUserScalable = flag; + } + synchronized void setSyntheticLinksEnabled(boolean flag) { if (mSyntheticLinksEnabled != flag) { mSyntheticLinksEnabled = flag; diff --git a/core/java/android/webkit/WebTextView.java b/core/java/android/webkit/WebTextView.java index 0f24edc..7f4f103 100644 --- a/core/java/android/webkit/WebTextView.java +++ b/core/java/android/webkit/WebTextView.java @@ -51,8 +51,8 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; import android.widget.AbsoluteLayout.LayoutParams; import android.widget.AdapterView; import android.widget.ArrayAdapter; @@ -515,7 +515,6 @@ import junit.framework.Assert; int candEnd = EditableInputConnection.getComposingSpanEnd(sp); imm.updateSelection(this, selStart, selEnd, candStart, candEnd); } - updateCursorControllerPositions(); } @Override diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index cf83456..f774803 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -27,6 +27,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.res.AssetManager; import android.content.res.Configuration; import android.database.DataSetObserver; import android.graphics.Bitmap; @@ -821,6 +822,8 @@ public class WebView extends AbsoluteLayout private WebViewCore.AutoFillData mAutoFillData; + private static boolean sNotificationsEnabled = true; + /** * URI scheme for telephone number */ @@ -874,8 +877,9 @@ public class WebView extends AbsoluteLayout */ public static final int UNKNOWN_TYPE = 0; /** - * HitTestResult for hitting a HTML::a tag + * @deprecated This type is no longer used. */ + @Deprecated public static final int ANCHOR_TYPE = 1; /** * HitTestResult for hitting a phone number @@ -894,8 +898,9 @@ public class WebView extends AbsoluteLayout */ public static final int IMAGE_TYPE = 5; /** - * HitTestResult for hitting a HTML::a tag which contains HTML::img + * @deprecated This type is no longer used. */ + @Deprecated public static final int IMAGE_ANCHOR_TYPE = 6; /** * HitTestResult for hitting a HTML::a tag with src=http @@ -987,6 +992,7 @@ public class WebView extends AbsoluteLayout protected WebView(Context context, AttributeSet attrs, int defStyle, Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { super(context, attrs, defStyle); + checkThread(); // Used by the chrome stack to find application paths JniUtil.setContext(context); @@ -1024,24 +1030,38 @@ public class WebView extends AbsoluteLayout } /* - * A variable to track if there is a receiver added for PROXY_CHANGE_ACTION + * Receiver for PROXY_CHANGE_ACTION, will be null when it is not added handling broadcasts. */ - private static boolean sProxyReceiverAdded; + private static ProxyReceiver sProxyReceiver; + /* + * @param context This method expects this to be a valid context + */ private static synchronized void setupProxyListener(Context context) { - if (sProxyReceiverAdded) { + if (sProxyReceiver != null || sNotificationsEnabled == false) { return; } IntentFilter filter = new IntentFilter(); filter.addAction(Proxy.PROXY_CHANGE_ACTION); + sProxyReceiver = new ProxyReceiver(); Intent currentProxy = context.getApplicationContext().registerReceiver( - new ProxyReceiver(), filter); - sProxyReceiverAdded = true; + sProxyReceiver, filter); if (currentProxy != null) { handleProxyBroadcast(currentProxy); } } + /* + * @param context This method expects this to be a valid context + */ + private static synchronized void disableProxyListener(Context context) { + if (sProxyReceiver == null) + return; + + context.getApplicationContext().unregisterReceiver(sProxyReceiver); + sProxyReceiver = null; + } + private static void handleProxyBroadcast(Intent intent) { ProxyProperties proxyProperties = (ProxyProperties)intent.getExtra(Proxy.EXTRA_PROXY_INFO); if (proxyProperties == null || proxyProperties.getHost() == null) { @@ -1139,7 +1159,7 @@ public class WebView extends AbsoluteLayout PackageInfo pInfo = pm.getPackageInfo(name, PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES); installedPackages.add(name); - } catch(PackageManager.NameNotFoundException e) { + } catch (PackageManager.NameNotFoundException e) { // package not found } } @@ -1194,6 +1214,11 @@ public class WebView extends AbsoluteLayout mHTML5VideoViewProxy = null ; } + @Override + public boolean shouldDelayChildPressedState() { + return true; + } + /** * Adds accessibility APIs to JavaScript. * @@ -1310,6 +1335,7 @@ public class WebView extends AbsoluteLayout * @param overlay TRUE if horizontal scrollbar should have overlay style. */ public void setHorizontalScrollbarOverlay(boolean overlay) { + checkThread(); mOverlayHorizontalScrollbar = overlay; } @@ -1318,6 +1344,7 @@ public class WebView extends AbsoluteLayout * @param overlay TRUE if vertical scrollbar should have overlay style. */ public void setVerticalScrollbarOverlay(boolean overlay) { + checkThread(); mOverlayVerticalScrollbar = overlay; } @@ -1326,6 +1353,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if horizontal scrollbar has overlay style. */ public boolean overlayHorizontalScrollbar() { + checkThread(); return mOverlayHorizontalScrollbar; } @@ -1334,6 +1362,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if vertical scrollbar has overlay style. */ public boolean overlayVerticalScrollbar() { + checkThread(); return mOverlayVerticalScrollbar; } @@ -1365,6 +1394,7 @@ public class WebView extends AbsoluteLayout * @deprecated This method is now obsolete. */ public int getVisibleTitleHeight() { + checkThread(); // need to restrict mScrollY due to over scroll return Math.max(getTitleHeight() - Math.max(0, mScrollY), 0); } @@ -1391,6 +1421,7 @@ public class WebView extends AbsoluteLayout * there is no certificate (the site is not secure). */ public SslCertificate getCertificate() { + checkThread(); return mCertificate; } @@ -1398,6 +1429,7 @@ public class WebView extends AbsoluteLayout * Sets the SSL certificate for the main top-level page. */ public void setCertificate(SslCertificate certificate) { + checkThread(); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "setCertificate=" + certificate); } @@ -1417,6 +1449,7 @@ public class WebView extends AbsoluteLayout * @param password The password for the given host. */ public void savePassword(String host, String username, String password) { + checkThread(); mDatabase.setUsernamePassword(host, username, password); } @@ -1431,6 +1464,7 @@ public class WebView extends AbsoluteLayout */ public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { + checkThread(); mDatabase.setHttpAuthUsernamePassword(host, realm, username, password); } @@ -1444,6 +1478,7 @@ public class WebView extends AbsoluteLayout * String[1] is password. Return null if it can't find anything. */ public String[] getHttpAuthUsernamePassword(String host, String realm) { + checkThread(); return mDatabase.getHttpAuthUsernamePassword(host, realm); } @@ -1476,6 +1511,11 @@ public class WebView extends AbsoluteLayout * methods may be called on a WebView after destroy. */ public void destroy() { + checkThread(); + destroyImpl(); + } + + private void destroyImpl() { clearHelpers(); if (mListBoxDialog != null) { mListBoxDialog.dismiss(); @@ -1510,21 +1550,38 @@ public class WebView extends AbsoluteLayout /** * Enables platform notifications of data state and proxy changes. - * @deprecated Obsolete - platform notifications are always enabled. + * Notifications are enabled by default. + * + * @deprecated This method is now obsolete. */ @Deprecated public static void enablePlatformNotifications() { - Network.enablePlatformNotifications(); + checkThread(); + synchronized (WebView.class) { + Network.enablePlatformNotifications(); + sNotificationsEnabled = true; + Context context = JniUtil.getContext(); + if (context != null) + setupProxyListener(context); + } } /** - * If platform notifications are enabled, this should be called - * from the Activity's onPause() or onStop(). - * @deprecated Obsolete - platform notifications are always enabled. + * Disables platform notifications of data state and proxy changes. + * Notifications are enabled by default. + * + * @deprecated This method is now obsolete. */ @Deprecated public static void disablePlatformNotifications() { - Network.disablePlatformNotifications(); + checkThread(); + synchronized (WebView.class) { + Network.disablePlatformNotifications(); + sNotificationsEnabled = false; + Context context = JniUtil.getContext(); + if (context != null) + disableProxyListener(context); + } } /** @@ -1535,6 +1592,7 @@ public class WebView extends AbsoluteLayout * @hide pending API solidification */ public void setJsFlags(String flags) { + checkThread(); mWebViewCore.sendMessage(EventHub.SET_JS_FLAGS, flags); } @@ -1545,6 +1603,7 @@ public class WebView extends AbsoluteLayout * @param networkUp boolean indicating if network is available */ public void setNetworkAvailable(boolean networkUp) { + checkThread(); mWebViewCore.sendMessage(EventHub.SET_NETWORK_STATE, networkUp ? 1 : 0, 0); } @@ -1554,6 +1613,7 @@ public class WebView extends AbsoluteLayout * {@hide} */ public void setNetworkType(String type, String subtype) { + checkThread(); Map<String, String> map = new HashMap<String, String>(); map.put("type", type); map.put("subtype", subtype); @@ -1573,6 +1633,7 @@ public class WebView extends AbsoluteLayout * @see #restorePicture */ public WebBackForwardList saveState(Bundle outState) { + checkThread(); if (outState == null) { return null; } @@ -1629,6 +1690,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public boolean savePicture(Bundle b, final File dest) { + checkThread(); if (dest == null || b == null) { return false; } @@ -1693,6 +1755,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public boolean restorePicture(Bundle b, File src) { + checkThread(); if (src == null || b == null) { return false; } @@ -1745,6 +1808,7 @@ public class WebView extends AbsoluteLayout * @see #restorePicture */ public WebBackForwardList restoreState(Bundle inState) { + checkThread(); WebBackForwardList returnList = null; if (inState == null) { return returnList; @@ -1804,6 +1868,11 @@ public class WebView extends AbsoluteLayout * will be replaced by the intrinsic value of the WebView. */ public void loadUrl(String url, Map<String, String> extraHeaders) { + checkThread(); + loadUrlImpl(url, extraHeaders); + } + + private void loadUrlImpl(String url, Map<String, String> extraHeaders) { switchOutDrawHistory(); WebViewCore.GetUrlData arg = new WebViewCore.GetUrlData(); arg.mUrl = url; @@ -1817,10 +1886,15 @@ public class WebView extends AbsoluteLayout * @param url The url of the resource to load. */ public void loadUrl(String url) { + checkThread(); + loadUrlImpl(url); + } + + private void loadUrlImpl(String url) { if (url == null) { return; } - loadUrl(url, null); + loadUrlImpl(url, null); } /** @@ -1832,6 +1906,7 @@ public class WebView extends AbsoluteLayout * @param postData The data will be passed to "POST" request. */ public void postUrl(String url, byte[] postData) { + checkThread(); if (URLUtil.isNetworkUrl(url)) { switchOutDrawHistory(); WebViewCore.PostUrlData arg = new WebViewCore.PostUrlData(); @@ -1840,7 +1915,7 @@ public class WebView extends AbsoluteLayout mWebViewCore.sendMessage(EventHub.POST_URL, arg); clearHelpers(); } else { - loadUrl(url); + loadUrlImpl(url); } } @@ -1855,7 +1930,12 @@ public class WebView extends AbsoluteLayout * @param encoding The encoding of the data. i.e. utf-8, base64 */ public void loadData(String data, String mimeType, String encoding) { - loadUrl("data:" + mimeType + ";" + encoding + "," + data); + checkThread(); + loadDataImpl(data, mimeType, encoding); + } + + private void loadDataImpl(String data, String mimeType, String encoding) { + loadUrlImpl("data:" + mimeType + ";" + encoding + "," + data); } /** @@ -1881,9 +1961,10 @@ public class WebView extends AbsoluteLayout */ public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { + checkThread(); if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) { - loadData(data, mimeType, encoding); + loadDataImpl(data, mimeType, encoding); return; } switchOutDrawHistory(); @@ -1903,7 +1984,8 @@ public class WebView extends AbsoluteLayout * @param filename The filename where the archive should be placed. */ public void saveWebArchive(String filename) { - saveWebArchive(filename, false, null); + checkThread(); + saveWebArchiveImpl(filename, false, null); } /* package */ static class SaveWebArchiveMessage { @@ -1932,6 +2014,12 @@ public class WebView extends AbsoluteLayout * file failed. */ public void saveWebArchive(String basename, boolean autoname, ValueCallback<String> callback) { + checkThread(); + saveWebArchiveImpl(basename, autoname, callback); + } + + private void saveWebArchiveImpl(String basename, boolean autoname, + ValueCallback<String> callback) { mWebViewCore.sendMessage(EventHub.SAVE_WEBARCHIVE, new SaveWebArchiveMessage(basename, autoname, callback)); } @@ -1940,6 +2028,7 @@ public class WebView extends AbsoluteLayout * Stop the current load. */ public void stopLoading() { + checkThread(); // TODO: should we clear all the messages in the queue before sending // STOP_LOADING? switchOutDrawHistory(); @@ -1950,6 +2039,7 @@ public class WebView extends AbsoluteLayout * Reload the current url. */ public void reload() { + checkThread(); clearHelpers(); switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.RELOAD); @@ -1960,6 +2050,7 @@ public class WebView extends AbsoluteLayout * @return True iff this WebView has a back history item. */ public boolean canGoBack() { + checkThread(); WebBackForwardList l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { @@ -1974,7 +2065,8 @@ public class WebView extends AbsoluteLayout * Go back in the history of this WebView. */ public void goBack() { - goBackOrForward(-1); + checkThread(); + goBackOrForwardImpl(-1); } /** @@ -1982,6 +2074,7 @@ public class WebView extends AbsoluteLayout * @return True iff this Webview has a forward history item. */ public boolean canGoForward() { + checkThread(); WebBackForwardList l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { @@ -1996,7 +2089,8 @@ public class WebView extends AbsoluteLayout * Go forward in the history of this WebView. */ public void goForward() { - goBackOrForward(1); + checkThread(); + goBackOrForwardImpl(1); } /** @@ -2006,6 +2100,7 @@ public class WebView extends AbsoluteLayout * history. */ public boolean canGoBackOrForward(int steps) { + checkThread(); WebBackForwardList l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { @@ -2025,6 +2120,11 @@ public class WebView extends AbsoluteLayout * forward list. */ public void goBackOrForward(int steps) { + checkThread(); + goBackOrForwardImpl(steps); + } + + private void goBackOrForwardImpl(int steps) { goBackOrForward(steps, false); } @@ -2040,6 +2140,7 @@ public class WebView extends AbsoluteLayout * Returns true if private browsing is enabled in this WebView. */ public boolean isPrivateBrowsingEnabled() { + checkThread(); return getSettings().isPrivateBrowsingEnabled(); } @@ -2062,6 +2163,7 @@ public class WebView extends AbsoluteLayout * @return true if the page was scrolled */ public boolean pageUp(boolean top) { + checkThread(); if (mNativeClass == 0) { return false; } @@ -2088,6 +2190,7 @@ public class WebView extends AbsoluteLayout * @return true if the page was scrolled */ public boolean pageDown(boolean bottom) { + checkThread(); if (mNativeClass == 0) { return false; } @@ -2112,6 +2215,7 @@ public class WebView extends AbsoluteLayout * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY */ public void clearView() { + checkThread(); mContentWidth = 0; mContentHeight = 0; setBaseLayer(0, null, false, false); @@ -2128,6 +2232,7 @@ public class WebView extends AbsoluteLayout * bounds of the view. */ public Picture capturePicture() { + checkThread(); if (mNativeClass == 0) return null; Picture result = new Picture(); nativeCopyBaseContentToPicture(result); @@ -2158,6 +2263,7 @@ public class WebView extends AbsoluteLayout * @return The current scale. */ public float getScale() { + checkThread(); return mZoomManager.getScale(); } @@ -2170,6 +2276,7 @@ public class WebView extends AbsoluteLayout * @param scaleInPercent The initial scale in percent. */ public void setInitialScale(int scaleInPercent) { + checkThread(); mZoomManager.setInitialScaleInPercent(scaleInPercent); } @@ -2179,6 +2286,7 @@ public class WebView extends AbsoluteLayout * level of this WebView. */ public void invokeZoomPicker() { + checkThread(); if (!getSettings().supportZoom()) { Log.w(LOGTAG, "This WebView doesn't support zoom."); return; @@ -2206,6 +2314,7 @@ public class WebView extends AbsoluteLayout * HitTestResult type is set to UNKNOWN_TYPE. */ public HitTestResult getHitTestResult() { + checkThread(); return hitTestResult(mInitialHitTestResult); } @@ -2287,6 +2396,7 @@ public class WebView extends AbsoluteLayout * - "src" returns the image's src attribute. */ public void requestFocusNodeHref(Message hrefMsg) { + checkThread(); if (hrefMsg == null) { return; } @@ -2315,6 +2425,7 @@ public class WebView extends AbsoluteLayout * as the data member with "url" as key. The result can be null. */ public void requestImageRef(Message msg) { + checkThread(); if (0 == mNativeClass) return; // client isn't initialized int contentX = viewToContentX(mLastTouchX + mScrollX); int contentY = viewToContentY(mLastTouchY + mScrollY); @@ -2393,6 +2504,8 @@ public class WebView extends AbsoluteLayout */ public void setTitleBarGravity(int gravity) { mTitleGravity = gravity; + // force refresh + invalidate(); } /** @@ -2809,6 +2922,7 @@ public class WebView extends AbsoluteLayout * @return The url for the current page. */ public String getUrl() { + checkThread(); WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getUrl() : null; } @@ -2822,6 +2936,7 @@ public class WebView extends AbsoluteLayout * @return The url that was originally requested for the current page. */ public String getOriginalUrl() { + checkThread(); WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getOriginalUrl() : null; } @@ -2832,6 +2947,7 @@ public class WebView extends AbsoluteLayout * @return The title for the current page. */ public String getTitle() { + checkThread(); WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getTitle() : null; } @@ -2842,6 +2958,7 @@ public class WebView extends AbsoluteLayout * @return The favicon for the current page. */ public Bitmap getFavicon() { + checkThread(); WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getFavicon() : null; } @@ -2862,6 +2979,7 @@ public class WebView extends AbsoluteLayout * @return The progress for the current page between 0 and 100. */ public int getProgress() { + checkThread(); return mCallbackProxy.getProgress(); } @@ -2869,6 +2987,7 @@ public class WebView extends AbsoluteLayout * @return the height of the HTML content. */ public int getContentHeight() { + checkThread(); return mContentHeight; } @@ -2886,6 +3005,7 @@ public class WebView extends AbsoluteLayout * useful if the application has been paused. */ public void pauseTimers() { + checkThread(); mWebViewCore.sendMessage(EventHub.PAUSE_TIMERS); } @@ -2894,6 +3014,7 @@ public class WebView extends AbsoluteLayout * This will resume dispatching all timers. */ public void resumeTimers() { + checkThread(); mWebViewCore.sendMessage(EventHub.RESUME_TIMERS); } @@ -2906,6 +3027,7 @@ public class WebView extends AbsoluteLayout * Note that this differs from pauseTimers(), which affects all WebViews. */ public void onPause() { + checkThread(); if (!mIsPaused) { mIsPaused = true; mWebViewCore.sendMessage(EventHub.ON_PAUSE); @@ -2921,6 +3043,7 @@ public class WebView extends AbsoluteLayout * Call this to resume a WebView after a previous call to onPause(). */ public void onResume() { + checkThread(); if (mIsPaused) { mIsPaused = false; mWebViewCore.sendMessage(EventHub.ON_RESUME); @@ -2941,6 +3064,7 @@ public class WebView extends AbsoluteLayout * free any available memory. */ public void freeMemory() { + checkThread(); mWebViewCore.sendMessage(EventHub.FREE_MEMORY); } @@ -2951,6 +3075,7 @@ public class WebView extends AbsoluteLayout * @param includeDiskFiles If false, only the RAM cache is cleared. */ public void clearCache(boolean includeDiskFiles) { + checkThread(); // Note: this really needs to be a static method as it clears cache for all // WebView. But we need mWebViewCore to send message to WebCore thread, so // we can't make this static. @@ -2963,6 +3088,7 @@ public class WebView extends AbsoluteLayout * currently focused textfield if there is one. */ public void clearFormData() { + checkThread(); if (inEditingMode()) { AutoCompleteAdapter adapter = null; mWebTextView.setAdapterCustom(adapter); @@ -2973,6 +3099,7 @@ public class WebView extends AbsoluteLayout * Tell the WebView to clear its internal back/forward list. */ public void clearHistory() { + checkThread(); mCallbackProxy.getBackForwardList().setClearPending(); mWebViewCore.sendMessage(EventHub.CLEAR_HISTORY); } @@ -2982,6 +3109,7 @@ public class WebView extends AbsoluteLayout * certificate errors. */ public void clearSslPreferences() { + checkThread(); mWebViewCore.sendMessage(EventHub.CLEAR_SSL_PREF_TABLE); } @@ -2994,6 +3122,7 @@ public class WebView extends AbsoluteLayout * updated to reflect any new state. */ public WebBackForwardList copyBackForwardList() { + checkThread(); return mCallbackProxy.getBackForwardList().clone(); } @@ -3005,6 +3134,7 @@ public class WebView extends AbsoluteLayout * @param forward Direction to search. */ public void findNext(boolean forward) { + checkThread(); if (0 == mNativeClass) return; // client isn't initialized nativeFindNext(forward); } @@ -3016,6 +3146,7 @@ public class WebView extends AbsoluteLayout * that were found. */ public int findAll(String find) { + checkThread(); if (0 == mNativeClass) return 0; // client isn't initialized int result = find != null ? nativeFindAll(find.toLowerCase(), find.toUpperCase(), find.equalsIgnoreCase(mLastFind)) : 0; @@ -3035,6 +3166,7 @@ public class WebView extends AbsoluteLayout * @return boolean True if the find dialog is shown, false otherwise. */ public boolean showFindDialog(String text, boolean showIme) { + checkThread(); FindActionModeCallback callback = new FindActionModeCallback(mContext); if (getParent() == null || startActionMode(callback) == null) { // Could not start the action mode, so end Find on page @@ -3111,6 +3243,7 @@ public class WebView extends AbsoluteLayout * @return the address, or if no address is found, return null. */ public static String findAddress(String addr) { + checkThread(); return findAddress(addr, false); } @@ -3144,6 +3277,7 @@ public class WebView extends AbsoluteLayout * Clear the highlighting surrounding text matches created by findAll. */ public void clearMatches() { + checkThread(); if (mNativeClass == 0) return; nativeSetFindIsEmpty(); @@ -3173,6 +3307,7 @@ public class WebView extends AbsoluteLayout * @param response The message that will be dispatched with the result. */ public void documentHasImages(Message response) { + checkThread(); if (response == null) { return; } @@ -3568,6 +3703,7 @@ public class WebView extends AbsoluteLayout * @param client An implementation of WebViewClient. */ public void setWebViewClient(WebViewClient client) { + checkThread(); mCallbackProxy.setWebViewClient(client); } @@ -3588,6 +3724,7 @@ public class WebView extends AbsoluteLayout * @param listener An implementation of DownloadListener. */ public void setDownloadListener(DownloadListener listener) { + checkThread(); mCallbackProxy.setDownloadListener(listener); } @@ -3598,6 +3735,7 @@ public class WebView extends AbsoluteLayout * @param client An implementation of WebChromeClient. */ public void setWebChromeClient(WebChromeClient client) { + checkThread(); mCallbackProxy.setWebChromeClient(client); } @@ -3638,6 +3776,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public void setPictureListener(PictureListener listener) { + checkThread(); mPictureListener = listener; } @@ -3679,6 +3818,7 @@ public class WebView extends AbsoluteLayout * JavaScript. */ public void addJavascriptInterface(Object obj, String interfaceName) { + checkThread(); if (obj == null) { return; } @@ -3693,6 +3833,7 @@ public class WebView extends AbsoluteLayout * @param interfaceName The name of the interface to remove. */ public void removeJavascriptInterface(String interfaceName) { + checkThread(); if (mWebViewCore != null) { WebViewCore.JSInterfaceData arg = new WebViewCore.JSInterfaceData(); arg.mInterfaceName = interfaceName; @@ -3707,6 +3848,7 @@ public class WebView extends AbsoluteLayout * settings. */ public WebSettings getSettings() { + checkThread(); return (mWebViewCore != null) ? mWebViewCore.getSettings() : null; } @@ -3719,6 +3861,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public static synchronized PluginList getPluginList() { + checkThread(); return new PluginList(); } @@ -3727,7 +3870,9 @@ public class WebView extends AbsoluteLayout * @deprecated This was used for Gears, which has been deprecated. */ @Deprecated - public void refreshPlugins(boolean reloadOpenPages) { } + public void refreshPlugins(boolean reloadOpenPages) { + checkThread(); + } //------------------------------------------------------------------------- // Override View methods @@ -3736,7 +3881,7 @@ public class WebView extends AbsoluteLayout @Override protected void finalize() throws Throwable { try { - destroy(); + destroyImpl(); } finally { super.finalize(); } @@ -4235,15 +4380,20 @@ public class WebView extends AbsoluteLayout } WebViewCore.CursorData cursorData() { - WebViewCore.CursorData result = new WebViewCore.CursorData(); - result.mMoveGeneration = nativeMoveGeneration(); - result.mFrame = nativeCursorFramePointer(); + WebViewCore.CursorData result = cursorDataNoPosition(); Point position = nativeCursorPosition(); result.mX = position.x; result.mY = position.y; return result; } + WebViewCore.CursorData cursorDataNoPosition() { + WebViewCore.CursorData result = new WebViewCore.CursorData(); + result.mMoveGeneration = nativeMoveGeneration(); + result.mFrame = nativeCursorFramePointer(); + return result; + } + /** * Delete text from start to end in the focused textfield. If there is no * focus, or if start == end, silently fail. If start and end are out of @@ -4999,6 +5149,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public void emulateShiftHeld() { + checkThread(); setUpSelect(false, 0, 0); } @@ -5442,6 +5593,18 @@ public class WebView extends AbsoluteLayout private static final int DRAG_LAYER_FINGER_DISTANCE = 20000; @Override + public boolean onHoverEvent(MotionEvent event) { + if (mNativeClass == 0) { + return false; + } + WebViewCore.CursorData data = cursorDataNoPosition(); + data.mX = viewToContentX((int) event.getX()); + data.mY = viewToContentY((int) event.getY()); + mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, data); + return true; + } + + @Override public boolean onTouchEvent(MotionEvent ev) { if (mNativeClass == 0 || (!isClickable() && !isLongClickable())) { return false; @@ -6268,6 +6431,7 @@ public class WebView extends AbsoluteLayout private boolean mMapTrackballToArrowKeys = true; public void setMapTrackballToArrowKeys(boolean setMap) { + checkThread(); mMapTrackballToArrowKeys = setMap; } @@ -6560,6 +6724,7 @@ public class WebView extends AbsoluteLayout } public void flingScroll(int vx, int vy) { + checkThread(); mScroller.fling(mScrollX, mScrollY, vx, vy, 0, computeMaxScrollX(), 0, computeMaxScrollY(), mOverflingDistance, mOverflingDistance); invalidate(); @@ -6695,6 +6860,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public View getZoomControls() { + checkThread(); if (!getSettings().supportZoom()) { Log.w(LOGTAG, "This WebView doesn't support zoom."); return null; @@ -6714,6 +6880,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if the WebView can be zoomed in. */ public boolean canZoomIn() { + checkThread(); return mZoomManager.canZoomIn(); } @@ -6721,6 +6888,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if the WebView can be zoomed out. */ public boolean canZoomOut() { + checkThread(); return mZoomManager.canZoomOut(); } @@ -6729,6 +6897,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if zoom in succeeds. FALSE if no zoom changes. */ public boolean zoomIn() { + checkThread(); return mZoomManager.zoomIn(); } @@ -6737,6 +6906,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if zoom out succeeds. FALSE if no zoom changes. */ public boolean zoomOut() { + checkThread(); return mZoomManager.zoomOut(); } @@ -7154,7 +7324,10 @@ public class WebView extends AbsoluteLayout cursorData(), 1000); } - /* package */ synchronized WebViewCore getWebViewCore() { + /** + * @hide + */ + public synchronized WebViewCore getWebViewCore() { return mWebViewCore; } @@ -7865,7 +8038,10 @@ public class WebView extends AbsoluteLayout } case WEBCORE_INITIALIZED_MSG_ID: // nativeCreate sets mNativeClass to a non-zero value - nativeCreate(msg.arg1); + String drawableDir = BrowserFrame.getRawResFilename( + BrowserFrame.DRAWABLEDIR, mContext); + AssetManager am = mContext.getAssets(); + nativeCreate(msg.arg1, drawableDir, am); break; case UPDATE_TEXTFIELD_TEXT_MSG_ID: // Make sure that the textfield is currently focused @@ -8694,6 +8870,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public void debugDump() { + checkThread(); nativeDebugDump(); mWebViewCore.sendMessage(EventHub.DUMP_NAVTREE); } @@ -8755,12 +8932,25 @@ public class WebView extends AbsoluteLayout return mViewManager; } + private static void checkThread() { + if (!"main".equals(Thread.currentThread().getName())) { + try { + throw new RuntimeException("A WebView method was called on thread '" + + Thread.currentThread().getName() + "'. " + + "All WebView methods must be called on the UI thread. " + + "Future versions of WebView may not support use on other threads."); + } catch (RuntimeException e) { + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + } + private native int nativeCacheHitFramePointer(); private native boolean nativeCacheHitIsPlugin(); private native Rect nativeCacheHitNodeBounds(); private native int nativeCacheHitNodePointer(); /* package */ native void nativeClearCursor(); - private native void nativeCreate(int ptr); + private native void nativeCreate(int ptr, String drawableDir, AssetManager am); private native int nativeCursorFramePointer(); private native Rect nativeCursorNodeBounds(); private native int nativeCursorNodePointer(); diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 0271695..e8083eb 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -48,7 +48,10 @@ import java.util.Set; import junit.framework.Assert; -final class WebViewCore { +/** + * @hide + */ +public final class WebViewCore { private static final String LOGTAG = "webcore"; @@ -905,7 +908,10 @@ final class WebViewCore { "REMOVE_JS_INTERFACE", // = 149; }; - class EventHub { + /** + * @hide + */ + public class EventHub { // Message Ids static final int REVEAL_SELECTION = 96; static final int REQUEST_LABEL = 97; @@ -936,7 +942,7 @@ final class WebViewCore { static final int DELETE_SELECTION = 122; static final int LISTBOX_CHOICES = 123; static final int SINGLE_LISTBOX_CHOICE = 124; - static final int MESSAGE_RELAY = 125; + public static final int MESSAGE_RELAY = 125; static final int SET_BACKGROUND_COLOR = 126; static final int SET_MOVE_FOCUS = 127; static final int SAVE_DOCUMENT_STATE = 128; @@ -1702,7 +1708,10 @@ final class WebViewCore { // If it needs WebCore, it has to send message. //------------------------------------------------------------------------- - void sendMessage(Message msg) { + /** + * @hide + */ + public void sendMessage(Message msg) { mEventHub.sendMessage(msg); } @@ -2253,6 +2262,27 @@ final class WebViewCore { // set the viewport settings from WebKit setViewportSettingsFromNative(); + if (mSettings.forceUserScalable()) { + mViewportUserScalable = true; + if (mViewportInitialScale > 0) { + if (mViewportMinimumScale > 0) { + mViewportMinimumScale = Math.min(mViewportMinimumScale, + mViewportInitialScale / 2); + } + if (mViewportMaximumScale > 0) { + mViewportMaximumScale = Math.max(mViewportMaximumScale, + mViewportInitialScale * 2); + } + } else { + if (mViewportMinimumScale > 0) { + mViewportMinimumScale = Math.min(mViewportMinimumScale, 50); + } + if (mViewportMaximumScale > 0) { + mViewportMaximumScale = Math.max(mViewportMaximumScale, 200); + } + } + } + // adjust the default scale to match the densityDpi float adjust = 1.0f; if (mViewportDensityDpi == -1) { @@ -2589,11 +2619,11 @@ final class WebViewCore { // called by JNI private Class<?> getPluginClass(String libName, String clsName) { - + if (mWebView == null) { return null; } - + PluginManager pluginManager = PluginManager.getInstance(null); String pkgName = pluginManager.getPluginsAPKName(libName); @@ -2601,7 +2631,7 @@ final class WebViewCore { Log.w(LOGTAG, "Unable to resolve " + libName + " to a plugin APK"); return null; } - + try { return pluginManager.getPluginClass(pkgName, clsName); } catch (NameNotFoundException e) { @@ -2656,7 +2686,7 @@ final class WebViewCore { view.mView = pluginView; return view; } - + // called by JNI. PluginWidget functions for creating an embedded View for // the surface drawing model. private ViewManager.ChildView addSurface(View pluginView, int x, int y, diff --git a/core/java/android/webkit/webdriver/By.java b/core/java/android/webkit/webdriver/By.java new file mode 100644 index 0000000..fa4fe74 --- /dev/null +++ b/core/java/android/webkit/webdriver/By.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +import java.util.List; + +/** + * Mechanism to locate elements within the DOM of the page. + * @hide + */ +public abstract class By { + public abstract WebElement findElement(WebElement element); + public abstract List<WebElement> findElements(WebElement element); + + /** + * Locates an element by its HTML id attribute. + * + * @param id The HTML id attribute to look for. + * @return A By instance that locates elements by their HTML id attributes. + */ + public static By id(final String id) { + throwIfNull(id); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementById(id); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsById(id); // Yes, it happens a lot. + } + + @Override + public String toString() { + return "By.id: " + id; + } + }; + } + + /** + * Locates an element by the matching the exact text on the HTML link. + * + * @param linkText The exact text to match against. + * @return A By instance that locates elements by the text displayed by + * the link. + */ + public static By linkText(final String linkText) { + throwIfNull(linkText); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByLinkText(linkText); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByLinkText(linkText); + } + + @Override + public String toString() { + return "By.linkText: " + linkText; + } + }; + } + + /** + * Locates an element by matching partial part of the text displayed by an + * HTML link. + * + * @param linkText The text that should be contained by the text displayed + * on the link. + * @return A By instance that locates elements that contain the given link + * text. + */ + public static By partialLinkText(final String linkText) { + throwIfNull(linkText); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByPartialLinkText(linkText); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByPartialLinkText(linkText); + } + + @Override + public String toString() { + return "By.partialLinkText: " + linkText; + } + }; + } + + /** + * Locates an element by matching its HTML name attribute. + * + * @param name The value of the HTML name attribute. + * @return A By instance that locates elements by the HTML name attribute. + */ + public static By name(final String name) { + throwIfNull(name); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByName(name); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByName(name); + } + + @Override + public String toString() { + return "By.name: " + name; + } + }; + } + + /** + * Locates an element by matching its class name. + * @param className The class name + * @return A By instance that locates elements by their class name attribute. + */ + public static By className(final String className) { + throwIfNull(className); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByClassName(className); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByClassName(className); + } + + @Override + public String toString() { + return "By.className: " + className; + } + }; + } + + /** + * Locates an element by matching its css property. + * + * @param css The css property. + * @return A By instance that locates elements by their css property. + */ + public static By css(final String css) { + throwIfNull(css); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByCss(css); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByCss(css); + } + + @Override + public String toString() { + return "By.css: " + css; + } + }; + } + + /** + * Locates an element by matching its HTML tag name. + * + * @param tagName The HTML tag name to look for. + * @return A By instance that locates elements using the name of the + * HTML tag. + */ + public static By tagName(final String tagName) { + throwIfNull(tagName); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByTagName(tagName); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByTagName(tagName); + } + + @Override + public String toString() { + return "By.tagName: " + tagName; + } + }; + } + + /** + * Locates an element using an XPath expression. + * + * <p>When using XPath, be aware that this follows standard conventions: a + * search prefixed with "//" will search the entire document, not just the + * children of the current node. Use ".//" to limit your search to the + * children of this {@link android.webkit.webdriver.WebElement}. + * + * @param xpath The XPath expression to use. + * @return A By instance that locates elements using the given XPath. + */ + public static By xpath(final String xpath) { + throwIfNull(xpath); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByXPath(xpath); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByXPath(xpath); + } + + @Override + public String toString() { + return "By.xpath: " + xpath; + } + }; + } + + private static void throwIfNull(String argument) { + if (argument == null) { + throw new IllegalArgumentException( + "Cannot find elements with null locator."); + } + } +} diff --git a/core/java/android/webkit/webdriver/WebDriver.java b/core/java/android/webkit/webdriver/WebDriver.java new file mode 100644 index 0000000..79e6523 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebDriver.java @@ -0,0 +1,843 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +import android.graphics.Point; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.webkit.WebView; +import android.webkit.WebViewCore; + +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; + +import com.android.internal.R; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Drives a web application by controlling the WebView. This class + * provides a DOM-like API allowing to get information about the page, + * navigate, and interact with the web application. This is particularly useful + * for testing a web application. + * + * <p/>{@link android.webkit.webdriver.WebDriver} should be created in the main + * thread, and invoked from another thread. Here is a sample usage: + * + * public class WebDriverStubActivity extends Activity { + * private WebDriver mDriver; + * + * public void onCreate(Bundle savedInstanceState) { + * super.onCreate(savedInstanceState); + * WebView view = new WebView(this); + * mDriver = new WebDriver(view); + * setContentView(view); + * } + * + * + * public WebDriver getDriver() { + * return mDriver; + * } + *} + * + * public class WebDriverTest extends + * ActivityInstrumentationTestCase2<WebDriverStubActivity>{ + * private WebDriver mDriver; + * + * public WebDriverTest() { + * super(WebDriverStubActivity.class); + * } + * + * protected void setUp() throws Exception { + * super.setUp(); + * mDriver = getActivity().getDriver(); + * } + * + * public void testGoogle() { + * mDriver.get("http://google.com"); + * WebElement searchBox = mDriver.findElement(By.name("q")); + * q.sendKeys("Cheese!"); + * q.submit(); + * assertTrue(mDriver.findElements(By.partialLinkText("Cheese")).size() > 0); + * } + *} + * + * @hide + */ +public class WebDriver { + // Timeout for page load in milliseconds. + private static final int LOADING_TIMEOUT = 30000; + // Timeout for executing JavaScript in the WebView in milliseconds. + private static final int JS_EXECUTION_TIMEOUT = 10000; + // Timeout for the MotionEvent to be completely handled + private static final int MOTION_EVENT_TIMEOUT = 1000; + // Timeout for detecting a new page load + private static final int PAGE_STARTED_LOADING = 500; + // Timeout for handling KeyEvents + private static final int KEY_EVENT_TIMEOUT = 2000; + + // Commands posted to the handler + private static final int CMD_GET_URL = 1; + private static final int CMD_EXECUTE_SCRIPT = 2; + private static final int CMD_SEND_TOUCH = 3; + private static final int CMD_SEND_KEYS = 4; + private static final int CMD_NAV_REFRESH = 5; + private static final int CMD_NAV_BACK = 6; + private static final int CMD_NAV_FORWARD = 7; + private static final int CMD_SEND_KEYCODE = 8; + private static final int CMD_MOVE_CURSOR_RIGHTMOST_POS = 9; + private static final int CMD_MESSAGE_RELAY_ECHO = 10; + + private static final String ELEMENT_KEY = "ELEMENT"; + private static final String STATUS = "status"; + private static final String VALUE = "value"; + + private static final long MAIN_THREAD = Thread.currentThread().getId(); + + // This is updated by a callabck from JavaScript when the result is ready. + private String mJsResult; + + // Used for synchronization + private final Object mSyncObject; + private final Object mSyncPageLoad; + + // Updated when the command is done executing in the main thread. + private volatile boolean mCommandDone; + // Used by WebViewClientWrapper.onPageStarted() to notify that + // a page started loading. + private volatile boolean mPageStartedLoading; + // Used by WebChromeClientWrapper.onProgressChanged to notify when + // a page finished loading. + private volatile boolean mPageFinishedLoading; + private WebView mWebView; + private Navigation mNavigation; + // This WebElement represents the object document.documentElement + private WebElement mDocumentElement; + + + // This Handler runs in the main UI thread. + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case CMD_GET_URL: + final String url = (String) msg.obj; + mWebView.loadUrl(url); + break; + case CMD_EXECUTE_SCRIPT: + mWebView.loadUrl("javascript:" + (String) msg.obj); + break; + case CMD_MESSAGE_RELAY_ECHO: + notifyCommandDone(); + break; + case CMD_SEND_TOUCH: + touchScreen((Point) msg.obj); + notifyCommandDone(); + break; + case CMD_SEND_KEYS: + dispatchKeys((CharSequence[]) msg.obj); + notifyCommandDone(); + break; + case CMD_NAV_REFRESH: + mWebView.reload(); + break; + case CMD_NAV_BACK: + mWebView.goBack(); + break; + case CMD_NAV_FORWARD: + mWebView.goForward(); + break; + case CMD_SEND_KEYCODE: + dispatchKeyCodes((int[]) msg.obj); + notifyCommandDone(); + break; + case CMD_MOVE_CURSOR_RIGHTMOST_POS: + moveCursorToLeftMostPos((String) msg.obj); + notifyCommandDone(); + break; + } + } + }; + + /** + * Error codes from the WebDriver wire protocol + * http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes + */ + private enum ErrorCode { + SUCCESS(0), + NO_SUCH_ELEMENT(7), + NO_SUCH_FRAME(8), + UNKNOWN_COMMAND(9), + UNSUPPORTED_OPERATION(9), // Alias + STALE_ELEMENT_REFERENCE(10), + ELEMENT_NOT_VISISBLE(11), + INVALID_ELEMENT_STATE(12), + UNKNOWN_ERROR(13), + ELEMENT_NOT_SELECTABLE(15), + XPATH_LOOKUP_ERROR(19), + NO_SUCH_WINDOW(23), + INVALID_COOKIE_DOMAIN(24), + UNABLE_TO_SET_COOKIE(25), + MODAL_DIALOG_OPENED(26), + MODAL_DIALOG_OPEN(27), + SCRIPT_TIMEOUT(28); + + private final int mCode; + private static ErrorCode[] values = ErrorCode.values(); + + ErrorCode(int code) { + this.mCode = code; + } + + public int getCode() { + return mCode; + } + + public static ErrorCode get(final int intValue) { + for (int i = 0; i < values.length; i++) { + if (values[i].getCode() == intValue) { + return values[i]; + } + } + return UNKNOWN_ERROR; + } + } + + public WebDriver(WebView webview) { + this.mWebView = webview; + mWebView.requestFocus(); + if (mWebView == null) { + throw new IllegalArgumentException("WebView cannot be null"); + } + if (!mWebView.getSettings().getJavaScriptEnabled()) { + throw new RuntimeException("Javascript is disabled in the WebView. " + + "Enable it to use WebDriver"); + } + shouldRunInMainThread(true); + + mSyncObject = new Object(); + mSyncPageLoad = new Object(); + this.mWebView = webview; + WebChromeClientWrapper chromeWrapper = new WebChromeClientWrapper( + webview.getWebChromeClient(), this); + mWebView.setWebChromeClient(chromeWrapper); + WebViewClientWrapper viewWrapper = new WebViewClientWrapper( + webview.getWebViewClient(), this); + mWebView.setWebViewClient(viewWrapper); + mWebView.addJavascriptInterface(new JavascriptResultReady(), + "webdriver"); + mDocumentElement = new WebElement(this, ""); + mNavigation = new Navigation(); + } + + /** + * @return The title of the current page, null if not set. + */ + public String getTitle() { + return mWebView.getTitle(); + } + + /** + * Loads a URL in the WebView. This function is blocking and will return + * when the page has finished loading. + * + * @param url The URL to load. + */ + public void get(String url) { + mNavigation.to(url); + } + + /** + * @return The source page of the currently loaded page in WebView. + */ + public String getPageSource() { + return (String) executeScript("return new XMLSerializer()." + + "serializeToString(document);"); + } + + /** + * Find the first {@link android.webkit.webdriver.WebElement} using the + * given method. + * + * @param by The locating mechanism to use. + * @return The first matching element on the current context. + * @throws {@link android.webkit.webdriver.WebElementNotFoundException} if + * no matching element was found. + */ + public WebElement findElement(By by) { + checkNotNull(mDocumentElement, "Load a page using WebDriver.get() " + + "before looking for elements."); + return by.findElement(mDocumentElement); + } + + /** + * Finds all {@link android.webkit.webdriver.WebElement} within the page + * using the given method. + * + * @param by The locating mechanism to use. + * @return A list of all {@link android.webkit.webdriver.WebElement} found, + * or an empty list if nothing matches. + */ + public List<WebElement> findElements(By by) { + checkNotNull(mDocumentElement, "Load a page using WebDriver.get() " + + "before looking for elements."); + return by.findElements(mDocumentElement); + } + + /** + * Clears the WebView's state and closes associated views. + */ + public void quit() { + mWebView.clearCache(true); + mWebView.clearFormData(); + mWebView.clearHistory(); + mWebView.clearSslPreferences(); + mWebView.clearView(); + mWebView.removeAllViewsInLayout(); + } + + /** + * Executes javascript in the context of the main frame. + * + * If the script has a return value the following happens: + * <ul> + * <li>For an HTML element, this method returns a WebElement</li> + * <li>For a decimal, a Double is returned</li> + * <li>For non-decimal number, a Long is returned</li> + * <li>For a boolean, a Boolean is returned</li> + * <li>For all other cases, a String is returned</li> + * <li>For an array, this returns a List<Object> with each object + * following the rules above.</li> + * <li>For an object literal this returns a Map<String, Object>. Note that + * Object literals keys can only be Strings. Non Strings keys will + * be filtered out.</li> + * </ul> + * + * <p> Arguments must be a number, a boolean, a string a WebElement or + * a list of any combination of the above. The arguments will be made + * available to the javascript via the "arguments" magic variable, + * as if the function was called via "Function.apply". + * + * @param script The JavaScript to execute. + * @param args The arguments to the script. Can be any of a number, boolean, + * string, WebElement or a List of those. + * @return A Boolean, Long, Double, String, WebElement, List or null. + */ + public Object executeScript(final String script, final Object... args) { + String scriptArgs = "[" + convertToJsArgs(args) + "]"; + String injectScriptJs = getResourceAsString(R.raw.execute_script_android); + return executeRawJavascript("(" + injectScriptJs + + ")(" + escapeAndQuote(script) + ", " + scriptArgs + ", true)"); + } + + public Navigation navigate() { + return mNavigation; + } + + + /** + * @hide + */ + public class Navigation { + /* package */ Navigation () {} + + public void back() { + navigate(CMD_NAV_BACK, null); + } + + public void forward() { + navigate(CMD_NAV_FORWARD, null); + } + + public void to(String url) { + navigate(CMD_GET_URL, url); + } + + public void refresh() { + navigate(CMD_NAV_REFRESH, null); + } + + private void navigate(int command, String url) { + synchronized (mSyncPageLoad) { + mPageFinishedLoading = false; + Message msg = mHandler.obtainMessage(command); + msg.obj = url; + mHandler.sendMessage(msg); + waitForPageLoad(); + } + } + } + + /** + * Converts the arguments passed to a JavaScript friendly format. + * + * @param args The arguments to convert. + * @return Comma separated Strings containing the arguments. + */ + /* package */ String convertToJsArgs(final Object... args) { + StringBuilder toReturn = new StringBuilder(); + int length = args.length; + for (int i = 0; i < length; i++) { + toReturn.append((i > 0) ? "," : ""); + if (args[i] instanceof List<?>) { + toReturn.append("["); + List<Object> aList = (List<Object>) args[i]; + for (int j = 0 ; j < aList.size(); j++) { + String comma = ((j == 0) ? "" : ","); + toReturn.append(comma + convertToJsArgs(aList.get(j))); + } + toReturn.append("]"); + } else if (args[i] instanceof Map<?, ?>) { + Map<Object, Object> aMap = (Map<Object, Object>) args[i]; + String toAdd = "{"; + for (Object key: aMap.keySet()) { + toAdd += key + ":" + + convertToJsArgs(aMap.get(key)) + ","; + } + toReturn.append(toAdd.substring(0, toAdd.length() -1) + "}"); + } else if (args[i] instanceof WebElement) { + // WebElement are represented in JavaScript by Objects as + // follow: {ELEMENT:"id"} where "id" refers to the id + // of the HTML element in the javascript cache that can + // be accessed throught bot.inject.cache.getCache_() + toReturn.append("{\"" + ELEMENT_KEY + "\":\"" + + ((WebElement) args[i]).getId() + "\"}"); + } else if (args[i] instanceof Number || args[i] instanceof Boolean) { + toReturn.append(String.valueOf(args[i])); + } else if (args[i] instanceof String) { + toReturn.append(escapeAndQuote((String) args[i])); + } else { + throw new IllegalArgumentException( + "Javascript arguments can be " + + "a Number, a Boolean, a String, a WebElement, " + + "or a List or a Map of those. Got: " + + ((args[i] == null) ? "null" : args[i].getClass() + + ", value: " + args[i].toString())); + } + } + return toReturn.toString(); + } + + /* package */ Object executeRawJavascript(final String script) { + if (mWebView.getUrl() == null) { + throw new WebDriverException("Cannot operate on a blank page. " + + "Load a page using WebDriver.get()."); + } + String result = executeCommand(CMD_EXECUTE_SCRIPT, + "if (!window.webdriver || !window.webdriver.resultReady) {" + + " return;" + + "}" + + "window.webdriver.resultReady(" + script + ")", + JS_EXECUTION_TIMEOUT); + if (result == null || "undefined".equals(result)) { + return null; + } + try { + JSONObject json = new JSONObject(result); + throwIfError(json); + Object value = json.get(VALUE); + return convertJsonToJavaObject(value); + } catch (JSONException e) { + throw new RuntimeException("Failed to parse JavaScript result: " + + result.toString(), e); + } + } + + /* package */ String getResourceAsString(final int resourceId) { + InputStream is = mWebView.getResources().openRawResource(resourceId); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line = null; + try { + while ((line = br.readLine()) != null) { + sb.append(line); + } + br.close(); + is.close(); + } catch (IOException e) { + throw new RuntimeException("Failed to open JavaScript resource.", e); + } + return sb.toString(); + } + + /* package */ void sendTouchScreen(Point coords) { + // Reset state + resetPageLoadState(); + executeCommand(CMD_SEND_TOUCH, coords,LOADING_TIMEOUT); + // Wait for the events to be fully handled + waitForMessageRelay(MOTION_EVENT_TIMEOUT); + + // If a page started loading, block until page finishes loading + waitForPageLoadIfNeeded(); + } + + /* package */ void resetPageLoadState() { + synchronized (mSyncPageLoad) { + mPageStartedLoading = false; + mPageFinishedLoading = false; + } + } + + /* package */ void waitForPageLoadIfNeeded() { + synchronized (mSyncPageLoad) { + Long end = System.currentTimeMillis() + PAGE_STARTED_LOADING; + // Wait PAGE_STARTED_LOADING milliseconds to see if we detect a + // page load. + while (!mPageStartedLoading && (System.currentTimeMillis() <= end)) { + try { + // This is notified by WebChromeClientWrapper#onProgressChanged + // when the page finished loading. + mSyncPageLoad.wait(PAGE_STARTED_LOADING); + } catch (InterruptedException e) { + new RuntimeException(e); + } + } + if (mPageStartedLoading) { + waitForPageLoad(); + } + } + } + + private void touchScreen(Point coords) { + // Convert to screen coords + // screen = JS x zoom - offset + float zoom = mWebView.getScale(); + float xOffset = mWebView.getX(); + float yOffset = mWebView.getY(); + Point screenCoords = new Point( (int)(coords.x*zoom - xOffset), + (int)(coords.y*zoom - yOffset)); + + long downTime = SystemClock.uptimeMillis(); + MotionEvent down = MotionEvent.obtain(downTime, + SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, screenCoords.x, + screenCoords.y, 0); + down.setSource(InputDevice.SOURCE_TOUCHSCREEN); + MotionEvent up = MotionEvent.obtain(downTime, + SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, screenCoords.x, + screenCoords.y, 0); + up.setSource(InputDevice.SOURCE_TOUCHSCREEN); + // Dispatch the events to WebView + mWebView.dispatchTouchEvent(down); + mWebView.dispatchTouchEvent(up); + } + + /* package */ void notifyPageStartedLoading() { + synchronized (mSyncPageLoad) { + mPageStartedLoading = true; + mSyncPageLoad.notify(); + } + } + + /* package */ void notifyPageFinishedLoading() { + synchronized (mSyncPageLoad) { + mPageFinishedLoading = true; + mSyncPageLoad.notify(); + } + } + + /** + * + * @param keys The first element of the CharSequence should be the + * existing value in the text input, or the empty string if none. + */ + /* package */ void sendKeys(CharSequence[] keys) { + executeCommand(CMD_SEND_KEYS, keys, KEY_EVENT_TIMEOUT); + // Wait for all KeyEvents to be handled + waitForMessageRelay(KEY_EVENT_TIMEOUT); + } + + /* package */ void sendKeyCodes(int[] keycodes) { + executeCommand(CMD_SEND_KEYCODE, keycodes, KEY_EVENT_TIMEOUT); + // Wait for all KeyEvents to be handled + waitForMessageRelay(KEY_EVENT_TIMEOUT); + } + + /* package */ void moveCursorToRightMostPosition(String value) { + executeCommand(CMD_MOVE_CURSOR_RIGHTMOST_POS, value, KEY_EVENT_TIMEOUT); + waitForMessageRelay(KEY_EVENT_TIMEOUT); + } + + private void moveCursorToLeftMostPos(String value) { + // If there is text, move the cursor to the rightmost position + if (value != null && !value.equals("")) { + long downTime = SystemClock.uptimeMillis(); + KeyEvent down = new KeyEvent(downTime, SystemClock.uptimeMillis(), + KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT, 0); + KeyEvent up = new KeyEvent(downTime, SystemClock.uptimeMillis(), + KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_RIGHT, + value.length()); + mWebView.dispatchKeyEvent(down); + mWebView.dispatchKeyEvent(up); + } + } + + private void dispatchKeyCodes(int[] keycodes) { + for (int i = 0; i < keycodes.length; i++) { + KeyEvent down = new KeyEvent(KeyEvent.ACTION_DOWN, keycodes[i]); + KeyEvent up = new KeyEvent(KeyEvent.ACTION_UP, keycodes[i]); + mWebView.dispatchKeyEvent(down); + mWebView.dispatchKeyEvent(up); + } + } + + private void dispatchKeys(CharSequence[] keys) { + KeyCharacterMap chararcterMap = KeyCharacterMap.load( + KeyCharacterMap.VIRTUAL_KEYBOARD); + for (int i = 0; i < keys.length; i++) { + CharSequence s = keys[i]; + for (int j = 0; j < s.length(); j++) { + KeyEvent[] events = + chararcterMap.getEvents(new char[]{s.charAt(j)}); + for (KeyEvent e : events) { + mWebView.dispatchKeyEvent(e); + } + } + } + } + + private void waitForMessageRelay(long timeout) { + synchronized (mSyncObject) { + mCommandDone = false; + } + Message msg = Message.obtain(); + msg.what = WebViewCore.EventHub.MESSAGE_RELAY; + Message echo = mHandler.obtainMessage(CMD_MESSAGE_RELAY_ECHO); + msg.obj = echo; + + mWebView.getWebViewCore().sendMessage(msg); + synchronized (mSyncObject) { + long end = System.currentTimeMillis() + timeout; + while (!mCommandDone && (System.currentTimeMillis() <= end)) { + try { + // This is notifed by the mHandler when it receives the + // MESSAGE_RELAY back + mSyncObject.wait(timeout); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + } + + private void waitForPageLoad() { + long endLoad = System.currentTimeMillis() + LOADING_TIMEOUT; + while (!mPageFinishedLoading + && (System.currentTimeMillis() <= endLoad)) { + try { + mSyncPageLoad.wait(LOADING_TIMEOUT); + } catch (InterruptedException e) { + throw new RuntimeException(); + } + } + } + + /** + * Wraps the given string into quotes and escape existing quotes + * and backslashes. + * "foo" -> "\"foo\"" + * "foo\"" -> "\"foo\\\"\"" + * "fo\o" -> "\"fo\\o\"" + * + * @param toWrap The String to wrap in quotes + * @return a String wrapping the original String in quotes + */ + private static String escapeAndQuote(final String toWrap) { + StringBuilder toReturn = new StringBuilder("\""); + for (int i = 0; i < toWrap.length(); i++) { + char c = toWrap.charAt(i); + if (c == '\"') { + toReturn.append("\\\""); + } else if (c == '\\') { + toReturn.append("\\\\"); + } else { + toReturn.append(c); + } + } + toReturn.append("\""); + return toReturn.toString(); + } + + private Object convertJsonToJavaObject(final Object toConvert) { + try { + if (toConvert == null + || toConvert.equals(null) + || "undefined".equals(toConvert) + || "null".equals(toConvert)) { + return null; + } else if (toConvert instanceof Boolean) { + return toConvert; + } else if (toConvert instanceof Double + || toConvert instanceof Float) { + return Double.valueOf(String.valueOf(toConvert)); + } else if (toConvert instanceof Integer + || toConvert instanceof Long) { + return Long.valueOf(String.valueOf(toConvert)); + } else if (toConvert instanceof JSONArray) { // List + return convertJsonArrayToList((JSONArray) toConvert); + } else if (toConvert instanceof JSONObject) { // Map or WebElment + JSONObject map = (JSONObject) toConvert; + if (map.opt(ELEMENT_KEY) != null) { // WebElement + return new WebElement(this, (String) map.get(ELEMENT_KEY)); + } else { // Map + return convertJsonObjectToMap(map); + } + } else { + return toConvert.toString(); + } + } catch (JSONException e) { + throw new RuntimeException("Failed to parse JavaScript result: " + + toConvert.toString(), e); + } + } + + private List<Object> convertJsonArrayToList(final JSONArray json) { + List<Object> toReturn = Lists.newArrayList(); + for (int i = 0; i < json.length(); i++) { + try { + toReturn.add(convertJsonToJavaObject(json.get(i))); + } catch (JSONException e) { + throw new RuntimeException("Failed to parse JSON: " + + json.toString(), e); + } + } + return toReturn; + } + + private Map<Object, Object> convertJsonObjectToMap(final JSONObject json) { + Map<Object, Object> toReturn = Maps.newHashMap(); + for (Iterator it = json.keys(); it.hasNext();) { + String key = (String) it.next(); + try { + Object value = json.get(key); + toReturn.put(convertJsonToJavaObject(key), + convertJsonToJavaObject(value)); + } catch (JSONException e) { + throw new RuntimeException("Failed to parse JSON:" + + json.toString(), e); + } + } + return toReturn; + } + + private void throwIfError(final JSONObject jsonObject) { + ErrorCode status; + String errorMsg; + try { + status = ErrorCode.get((Integer) jsonObject.get(STATUS)); + errorMsg = String.valueOf(jsonObject.get(VALUE)); + } catch (JSONException e) { + throw new RuntimeException("Failed to parse JSON Object: " + + jsonObject, e); + } + switch (status) { + case SUCCESS: + return; + case NO_SUCH_ELEMENT: + throw new WebElementNotFoundException("Could not find " + + "WebElement."); + case STALE_ELEMENT_REFERENCE: + throw new WebElementStaleException("WebElement is stale."); + default: + throw new WebDriverException("Error: " + errorMsg); + } + } + + private void shouldRunInMainThread(boolean value) { + assert (value == (MAIN_THREAD == Thread.currentThread().getId())); + } + + /** + * Interface called from JavaScript when the result is ready. + */ + private class JavascriptResultReady { + + /** + * A callback from JavaScript to Java that passes the result as a + * parameter. This method is available from the WebView's + * JavaScript DOM as window.webdriver.resultReady(). + * + * @param result The result that should be sent to Java from Javascript. + */ + public void resultReady(final String result) { + synchronized (mSyncObject) { + mJsResult = result; + mCommandDone = true; + mSyncObject.notify(); + } + } + } + + /* package */ void notifyCommandDone() { + synchronized (mSyncObject) { + mCommandDone = true; + mSyncObject.notify(); + } + } + + /** + * Executes the given command by posting a message to mHandler. This thread + * will block until the command which runs in the main thread is done. + * + * @param command The command to run. + * @param arg The argument for that command. + * @param timeout A timeout in milliseconds. + */ + private String executeCommand(int command, final Object arg, long timeout) { + shouldRunInMainThread(false); + synchronized (mSyncObject) { + mCommandDone = false; + Message msg = mHandler.obtainMessage(command); + msg.obj = arg; + mHandler.sendMessage(msg); + + long end = System.currentTimeMillis() + timeout; + while (!mCommandDone) { + if (System.currentTimeMillis() >= end) { + throw new RuntimeException("Timeout executing command: " + + command); + } + try { + mSyncObject.wait(timeout); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + return mJsResult; + } + + private void checkNotNull(Object obj, String errosMsg) { + if (obj == null) { + throw new NullPointerException(errosMsg); + } + } +} diff --git a/core/java/android/webkit/webdriver/WebDriverException.java b/core/java/android/webkit/webdriver/WebDriverException.java new file mode 100644 index 0000000..1a579c2 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebDriverException.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +/** + * @hide + */ +public class WebDriverException extends RuntimeException { + public WebDriverException() { + super(); + } + + public WebDriverException(String reason) { + super(reason); + } + + public WebDriverException(String reason, Throwable cause) { + super(reason, cause); + } + + public WebDriverException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/webkit/webdriver/WebElement.java b/core/java/android/webkit/webdriver/WebElement.java new file mode 100644 index 0000000..02c1595 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebElement.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +import android.graphics.Point; +import android.view.KeyEvent; + +import com.android.internal.R; + +import java.util.List; +import java.util.Map; + +/** + * Represents an HTML element. Typically most interactions with a web page + * will be performed through this class. + * + * @hide + */ +public class WebElement { + private final String mId; + private final WebDriver mDriver; + + private static final String LOCATOR_ID = "id"; + private static final String LOCATOR_LINK_TEXT = "linkText"; + private static final String LOCATOR_PARTIAL_LINK_TEXT = "partialLinkText"; + private static final String LOCATOR_NAME = "name"; + private static final String LOCATOR_CLASS_NAME = "className"; + private static final String LOCATOR_CSS = "css"; + private static final String LOCATOR_TAG_NAME = "tagName"; + private static final String LOCATOR_XPATH = "xpath"; + + /** + * Package constructor to prevent clients from creating a new WebElement + * instance. + * + * <p> A WebElement represents an HTML element on the page. + * The corresponding HTML element is stored in a JS cache in the page + * that can be accessed through JavaScript using "bot.inject.cache". + * + * @param driver The WebDriver instance to use. + * @param id The index of the HTML element in the JavaSctipt cache. + * document.documentElement object. + */ + /* package */ WebElement(final WebDriver driver, final String id) { + this.mId = id; + this.mDriver = driver; + } + + /** + * Finds the first {@link android.webkit.webdriver.WebElement} using the + * given method. + * + * @param by The locating mechanism to use. + * @return The first matching element on the current context. + */ + public WebElement findElement(final By by) { + return by.findElement(this); + } + + /** + * Finds all {@link android.webkit.webdriver.WebElement} within the page + * using the given method. + * + * @param by The locating mechanism to use. + * @return A list of all {@link android.webkit.webdriver.WebElement} found, + * or an empty list if nothing matches. + */ + public List<WebElement> findElements(final By by) { + return by.findElements(this); + } + + /** + * Gets the visisble (i.e. not hidden by CSS) innerText of this element, + * inlcuding sub-elements. + * + * @return the innerText of this element. + * @throws {@link android.webkit.webdriver.WebElementStaleException} if this + * element is stale, i.e. not on the current DOM. + */ + public String getText() { + String getText = mDriver.getResourceAsString(R.raw.get_text_android); + return (String) executeAtom(getText, this); + } + + /** + * Gets the value of an HTML attribute for this element or the value of the + * property with the same name if the attribute is not present. If neither + * is set, null is returned. + * + * @param attribute the HTML attribute. + * @return the value of that attribute or the value of the property with the + * same name if the attribute is not set, or null if neither are set. For + * boolean attribute values this will return the string "true" or "false". + */ + public String getAttribute(String attribute) { + String getAttribute = mDriver.getResourceAsString( + R.raw.get_attribute_value_android); + return (String) executeAtom(getAttribute, this, attribute); + } + + /** + * @return the tag name of this element. + */ + public String getTagName() { + return (String) mDriver.executeScript("return arguments[0].tagName;", + this); + } + + /** + * @return true if this element is enabled, false otherwise. + */ + public boolean isEnabled() { + String isEnabled = mDriver.getResourceAsString( + R.raw.is_enabled_android); + return (Boolean) executeAtom(isEnabled, this); + } + + /** + * Determines whether this element is selected or not. This applies to input + * elements such as checkboxes, options in a select, and radio buttons. + * + * @return True if this element is selected, false otherwise. + */ + public boolean isSelected() { + String isSelected = mDriver.getResourceAsString( + R.raw.is_selected_android); + return (Boolean) executeAtom(isSelected, this); + } + + /** + * Selects an element on the page. This works for selecting checkboxes, + * options in a select, and radio buttons. + */ + public void setSelected() { + String setSelected = mDriver.getResourceAsString( + R.raw.set_selected_android); + executeAtom(setSelected, this); + } + + /** + * This toggles the checkboxe state from selected to not selected, or + * from not selected to selected. + * + * @return True if the toggled element is selected, false otherwise. + */ + public boolean toggle() { + String toggle = mDriver.getResourceAsString(R.raw.toggle_android); + return (Boolean) executeAtom(toggle, this); + } + + /** + * Sends the KeyEvents for the given sequence of characters to the + * WebElement to simulate typing. The KeyEvents are generated using the + * device's {@link android.view.KeyCharacterMap.VIRTUAL_KEYBOARD}. + * + * @param keys The keys to send to this WebElement + */ + public void sendKeys(CharSequence... keys) { + if (keys == null || keys.length == 0) { + return; + } + click(); + mDriver.moveCursorToRightMostPosition(getAttribute("value")); + mDriver.sendKeys(keys); + } + + /** + * Use this to send one of the key code constants defined in + * {@link android.view.KeyEvent} + * + * @param keys + */ + public void sendKeyCodes(int... keys) { + if (keys == null || keys.length == 0) { + return; + } + click(); + mDriver.moveCursorToRightMostPosition(getAttribute("value")); + mDriver.sendKeyCodes(keys); + } + + /** + * Sends a touch event to the center coordinates of this WebElement. + */ + public void click() { + Point topLeft = getLocation(); + Point size = getSize(); + int jsX = topLeft.x + size.x/2; + int jsY = topLeft.y + size.y/2; + Point center = new Point(jsX, jsY); + mDriver.sendTouchScreen(center); + } + + /** + * Submits the form containing this WebElement. + */ + public void submit() { + mDriver.resetPageLoadState(); + String submit = mDriver.getResourceAsString(R.raw.submit_android); + executeAtom(submit, this); + mDriver.waitForPageLoadIfNeeded(); + } + + /** + * Clears the text value if this is a text entry element. Does nothing + * otherwise. + */ + public void clear() { + String value = getAttribute("value"); + if (value == null || value.equals("")) { + return; + } + int length = value.length(); + int[] keys = new int[length]; + for (int i = 0; i < length; i++) { + keys[i] = KeyEvent.KEYCODE_DEL; + } + sendKeyCodes(keys); + } + + /** + * @return the value of the given CSS property if found, null otherwise. + */ + public String getCssValue(String cssProperty) { + String getCssProp = mDriver.getResourceAsString( + R.raw.get_value_of_css_property_android); + return (String) executeAtom(getCssProp, this, cssProperty); + } + + /** + * Gets the width and height of the rendered element. + * + * @return a {@link android.graphics.Point}, where Point.x represents the + * width, and Point.y represents the height of the element. + */ + public Point getSize() { + String getSize = mDriver.getResourceAsString(R.raw.get_size_android); + Map<String, Long> map = (Map<String, Long>) executeAtom(getSize, this); + return new Point(map.get("width").intValue(), + map.get("height").intValue()); + } + + /** + * Gets the location of the top left corner of this element on the screen. + * If the element is not visisble, this will scroll to get the element into + * the visisble screen. + * + * @return a {@link android.graphics.Point} containing the x and y + * coordinates of the top left corner of this element. + */ + public Point getLocation() { + String getLocation = mDriver.getResourceAsString( + R.raw.get_top_left_coordinates_android); + Map<String,Long> map = (Map<String, Long>) executeAtom(getLocation, + this); + return new Point(map.get("x").intValue(), map.get("y").intValue()); + } + + /** + * @return True if the WebElement is displayed on the screen, + * false otherwise. + */ + public boolean isDisplayed() { + String isDisplayed = mDriver.getResourceAsString( + R.raw.is_displayed_android); + return (Boolean) executeAtom(isDisplayed, this); + } + + /*package*/ String getId() { + return mId; + } + + /* package */ WebElement findElementById(final String locator) { + return findElement(LOCATOR_ID, locator); + } + + /* package */ WebElement findElementByLinkText(final String linkText) { + return findElement(LOCATOR_LINK_TEXT, linkText); + } + + /* package */ WebElement findElementByPartialLinkText( + final String linkText) { + return findElement(LOCATOR_PARTIAL_LINK_TEXT, linkText); + } + + /* package */ WebElement findElementByName(final String name) { + return findElement(LOCATOR_NAME, name); + } + + /* package */ WebElement findElementByClassName(final String className) { + return findElement(LOCATOR_CLASS_NAME, className); + } + + /* package */ WebElement findElementByCss(final String css) { + return findElement(LOCATOR_CSS, css); + } + + /* package */ WebElement findElementByTagName(final String tagName) { + return findElement(LOCATOR_TAG_NAME, tagName); + } + + /* package */ WebElement findElementByXPath(final String xpath) { + return findElement(LOCATOR_XPATH, xpath); + } + + /* package */ List<WebElement> findElementsById(final String locator) { + return findElements(LOCATOR_ID, locator); + } + + /* package */ List<WebElement> findElementsByLinkText(final String linkText) { + return findElements(LOCATOR_LINK_TEXT, linkText); + } + + /* package */ List<WebElement> findElementsByPartialLinkText( + final String linkText) { + return findElements(LOCATOR_PARTIAL_LINK_TEXT, linkText); + } + + /* package */ List<WebElement> findElementsByName(final String name) { + return findElements(LOCATOR_NAME, name); + } + + /* package */ List<WebElement> findElementsByClassName(final String className) { + return findElements(LOCATOR_CLASS_NAME, className); + } + + /* package */ List<WebElement> findElementsByCss(final String css) { + return findElements(LOCATOR_CSS, css); + } + + /* package */ List<WebElement> findElementsByTagName(final String tagName) { + return findElements(LOCATOR_TAG_NAME, tagName); + } + + /* package */ List<WebElement> findElementsByXPath(final String xpath) { + return findElements(LOCATOR_XPATH, xpath); + } + + private Object executeAtom(final String atom, final Object... args) { + String scriptArgs = mDriver.convertToJsArgs(args); + return mDriver.executeRawJavascript("(" + + atom + ")(" + scriptArgs + ")"); + } + + private List<WebElement> findElements(String strategy, String locator) { + String findElements = mDriver.getResourceAsString( + R.raw.find_elements_android); + if (mId.equals("")) { + return (List<WebElement>) executeAtom(findElements, + strategy, locator); + } else { + return (List<WebElement>) executeAtom(findElements, + strategy, locator, this); + } + } + + private WebElement findElement(String strategy, String locator) { + String findElement = mDriver.getResourceAsString( + R.raw.find_element_android); + WebElement el; + if (mId.equals("")) { + el = (WebElement) executeAtom(findElement, + strategy, locator); + } else { + el = (WebElement) executeAtom(findElement, + strategy, locator, this); + } + if (el == null) { + throw new WebElementNotFoundException("Could not find element " + + "with " + strategy + ": " + locator); + } + return el; + } +} diff --git a/core/java/android/webkit/webdriver/WebElementNotFoundException.java b/core/java/android/webkit/webdriver/WebElementNotFoundException.java new file mode 100644 index 0000000..e66d279 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebElementNotFoundException.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +/** + * Thrown when a {@link android.webkit.webdriver.WebElement} is not found in the + * DOM of the page. + * @hide + */ +public class WebElementNotFoundException extends RuntimeException { + + public WebElementNotFoundException() { + super(); + } + + public WebElementNotFoundException(String reason) { + super(reason); + } + + public WebElementNotFoundException(String reason, Throwable cause) { + super(reason, cause); + } + + public WebElementNotFoundException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/webkit/webdriver/WebElementStaleException.java b/core/java/android/webkit/webdriver/WebElementStaleException.java new file mode 100644 index 0000000..c59e794 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebElementStaleException.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +/** + * Thrown when trying to access a {@link android.webkit.webdriver.WebElement} + * that is stale. This mean that the {@link android.webkit.webdriver.WebElement} + * is no longer present on the DOM of the page. + * @hide + */ +public class WebElementStaleException extends RuntimeException { + + public WebElementStaleException() { + super(); + } + + public WebElementStaleException(String reason) { + super(reason); + } + + public WebElementStaleException(String reason, Throwable cause) { + super(reason, cause); + } + + public WebElementStaleException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/webkit/webdriver/WebViewClient.java b/core/java/android/webkit/webdriver/WebViewClient.java new file mode 100644 index 0000000..c582b24 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebViewClient.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +import android.graphics.Bitmap; +import android.net.http.SslError; +import android.os.Message; +import android.view.KeyEvent; +import android.webkit.HttpAuthHandler; +import android.webkit.SslErrorHandler; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +/* package */ class WebViewClientWrapper extends WebViewClient { + private final WebViewClient mDelegate; + private final WebDriver mDriver; + + public WebViewClientWrapper(WebViewClient delegate, WebDriver driver) { + if (delegate == null) { + mDelegate = new WebViewClient(); + } else { + mDelegate = delegate; + } + this.mDriver = driver; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + return mDelegate.shouldOverrideUrlLoading(view, url); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + mDriver.notifyPageStartedLoading(); + mDelegate.onPageStarted(view, url, favicon); + } + + @Override + public void onPageFinished(WebView view, String url) { + mDelegate.onPageFinished(view, url); + } + + @Override + public void onLoadResource(WebView view, String url) { + mDelegate.onLoadResource(view, url); + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, + String url) { + return mDelegate.shouldInterceptRequest(view, url); + } + + @Override + public void onTooManyRedirects(WebView view, Message cancelMsg, + Message continueMsg) { + mDelegate.onTooManyRedirects(view, cancelMsg, continueMsg); + } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, + String failingUrl) { + mDelegate.onReceivedError(view, errorCode, description, failingUrl); + } + + @Override + public void onFormResubmission(WebView view, Message dontResend, + Message resend) { + mDelegate.onFormResubmission(view, dontResend, resend); + } + + @Override + public void doUpdateVisitedHistory(WebView view, String url, + boolean isReload) { + mDelegate.doUpdateVisitedHistory(view, url, isReload); + } + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, + SslError error) { + mDelegate.onReceivedSslError(view, handler, error); + } + + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, + String host, String realm) { + mDelegate.onReceivedHttpAuthRequest(view, handler, host, realm); + } + + @Override + public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { + return mDelegate.shouldOverrideKeyEvent(view, event); + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + mDelegate.onUnhandledKeyEvent(view, event); + } + + @Override + public void onScaleChanged(WebView view, float oldScale, float newScale) { + mDelegate.onScaleChanged(view, oldScale, newScale); + } + + @Override + public void onReceivedLoginRequest(WebView view, String realm, + String account, String args) { + mDelegate.onReceivedLoginRequest(view, realm, account, args); + } +} diff --git a/core/java/android/webkit/webdriver/WebchromeClientWrapper.java b/core/java/android/webkit/webdriver/WebchromeClientWrapper.java new file mode 100644 index 0000000..a9e5d19 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebchromeClientWrapper.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Message; +import android.view.View; +import android.webkit.ConsoleMessage; +import android.webkit.GeolocationPermissions; +import android.webkit.JsPromptResult; +import android.webkit.JsResult; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebStorage; +import android.webkit.WebView; + +/* package */ class WebChromeClientWrapper extends WebChromeClient { + + private final WebChromeClient mDelegate; + private final WebDriver mDriver; + + public WebChromeClientWrapper(WebChromeClient delegate, WebDriver driver) { + if (delegate == null) { + this.mDelegate = new WebChromeClient(); + } else { + this.mDelegate = delegate; + } + this.mDriver = driver; + } + + @Override + public void onProgressChanged(WebView view, int newProgress) { + if (newProgress == 100) { + mDriver.notifyPageFinishedLoading(); + } + mDelegate.onProgressChanged(view, newProgress); + } + + @Override + public void onReceivedTitle(WebView view, String title) { + mDelegate.onReceivedTitle(view, title); + } + + @Override + public void onReceivedIcon(WebView view, Bitmap icon) { + mDelegate.onReceivedIcon(view, icon); + } + + @Override + public void onReceivedTouchIconUrl(WebView view, String url, + boolean precomposed) { + mDelegate.onReceivedTouchIconUrl(view, url, precomposed); + } + + @Override + public void onShowCustomView(View view, + CustomViewCallback callback) { + mDelegate.onShowCustomView(view, callback); + } + + @Override + public void onHideCustomView() { + mDelegate.onHideCustomView(); + } + + @Override + public boolean onCreateWindow(WebView view, boolean dialog, + boolean userGesture, Message resultMsg) { + return mDelegate.onCreateWindow(view, dialog, userGesture, resultMsg); + } + + @Override + public void onRequestFocus(WebView view) { + mDelegate.onRequestFocus(view); + } + + @Override + public void onCloseWindow(WebView window) { + mDelegate.onCloseWindow(window); + } + + @Override + public boolean onJsAlert(WebView view, String url, String message, + JsResult result) { + return mDelegate.onJsAlert(view, url, message, result); + } + + @Override + public boolean onJsConfirm(WebView view, String url, String message, + JsResult result) { + return mDelegate.onJsConfirm(view, url, message, result); + } + + @Override + public boolean onJsPrompt(WebView view, String url, String message, + String defaultValue, JsPromptResult result) { + return mDelegate.onJsPrompt(view, url, message, defaultValue, result); + } + + @Override + public boolean onJsBeforeUnload(WebView view, String url, String message, + JsResult result) { + return mDelegate.onJsBeforeUnload(view, url, message, result); + } + + @Override + public void onExceededDatabaseQuota(String url, String databaseIdentifier, + long currentQuota, long estimatedSize, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + mDelegate.onExceededDatabaseQuota(url, databaseIdentifier, currentQuota, + estimatedSize, totalUsedQuota, quotaUpdater); + } + + @Override + public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + mDelegate.onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota, + quotaUpdater); + } + + @Override + public void onGeolocationPermissionsShowPrompt(String origin, + GeolocationPermissions.Callback callback) { + mDelegate.onGeolocationPermissionsShowPrompt(origin, callback); + } + + @Override + public void onGeolocationPermissionsHidePrompt() { + mDelegate.onGeolocationPermissionsHidePrompt(); + } + + @Override + public boolean onJsTimeout() { + return mDelegate.onJsTimeout(); + } + + @Override + public void onConsoleMessage(String message, int lineNumber, + String sourceID) { + mDelegate.onConsoleMessage(message, lineNumber, sourceID); + } + + @Override + public boolean onConsoleMessage(ConsoleMessage consoleMessage) { + return mDelegate.onConsoleMessage(consoleMessage); + } + + @Override + public Bitmap getDefaultVideoPoster() { + return mDelegate.getDefaultVideoPoster(); + } + + @Override + public View getVideoLoadingProgressView() { + return mDelegate.getVideoLoadingProgressView(); + } + + @Override + public void getVisitedHistory(ValueCallback<String[]> callback) { + mDelegate.getVisitedHistory(callback); + } + + @Override + public void openFileChooser(ValueCallback<Uri> uploadFile, + String acceptType) { + mDelegate.openFileChooser(uploadFile, acceptType); + } + + @Override + public void setInstallableWebApp() { + mDelegate.setInstallableWebApp(); + } + + @Override + public void setupAutoFill(Message msg) { + mDelegate.setupAutoFill(msg); + } +} diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index d39271e..f4300a5 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -55,6 +55,7 @@ import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityEvent; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -2532,6 +2533,21 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return mContextMenuInfo; } + /** @hide */ + @Override + public boolean showContextMenu(float x, float y, int metaState) { + final int position = pointToPosition((int)x, (int)y); + if (position != INVALID_POSITION) { + final long id = mAdapter.getItemId(position); + View child = getChildAt(position - mFirstPosition); + if (child != null) { + mContextMenuInfo = createContextMenuInfo(child, position, id); + return super.showContextMenuForChild(AbsListView.this); + } + } + return super.showContextMenu(x, y, metaState); + } + @Override public boolean showContextMenuForChild(View originalView) { final int longPressPosition = getPositionForView(originalView); @@ -2556,6 +2572,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } @Override + public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { + // Add a record for ourselves as well. + AccessibilityEvent record = AccessibilityEvent.obtain(); + // Set the class since it is not populated in #dispatchPopulateAccessibilityEvent + record.setClassName(getClass().getName()); + child.dispatchPopulateAccessibilityEvent(record); + event.appendRecord(record); + return true; + } + + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return false; } @@ -2806,7 +2833,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (ev.getEdgeFlags() != 0 && motionPosition < 0) { // If we couldn't find a view to click on, but the down event // was touching the edge, we will bail out and try again. - // This allows the edge correcting code in ViewRoot to try to + // This allows the edge correcting code in ViewAncestor to try to // find a nearby view to select return false; } @@ -2834,6 +2861,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te break; } } + + if (performButtonActionOnTouchDown(ev)) { + if (mTouchMode == TOUCH_MODE_DOWN) { + removeCallbacks(mPendingCheckForTap); + } + } break; } @@ -4529,8 +4562,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * Otherwise resurrects the selection and returns true if resurrected. */ boolean resurrectSelectionIfNeeded() { - if (mSelectedPosition < 0) { - return resurrectSelection(); + if (mSelectedPosition < 0 && resurrectSelection()) { + updateSelectorState(); + return true; } return false; } @@ -5008,7 +5042,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public boolean sendKeyEvent(KeyEvent event) { // Use our own input connection, since the filter // text view may not be shown in a window so has - // no ViewRoot to dispatch events with. + // no ViewAncestor to dispatch events with. return mDefInputConnection.sendKeyEvent(event); } }; diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index 0da73a4..2621e64 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -201,7 +201,8 @@ public abstract class AbsSeekBar extends ProgressBar { } @Override - void onProgressRefresh(float scale, boolean fromUser) { + void onProgressRefresh(float scale, boolean fromUser) { + super.onProgressRefresh(scale, fromUser); Drawable thumb = mThumb; if (thumb != null) { setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE); diff --git a/core/java/android/widget/AbsoluteLayout.java b/core/java/android/widget/AbsoluteLayout.java index ac82af7..7df6aab 100644 --- a/core/java/android/widget/AbsoluteLayout.java +++ b/core/java/android/widget/AbsoluteLayout.java @@ -141,6 +141,11 @@ public class AbsoluteLayout extends ViewGroup { return new LayoutParams(p); } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * Per-child layout information associated with AbsoluteLayout. * See diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index f16efbd..14ea853 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -876,7 +876,6 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = false; // This is an exceptional case which occurs when a window gets the // focus and sends a focus event via its focused child to announce // current focus/selection. AdapterView fires selection but not focus @@ -885,22 +884,29 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED); } - // we send selection events only from AdapterView to avoid - // generation of such event for each child + // We first get a chance to populate the event. + onPopulateAccessibilityEvent(event); + + // We send selection events only from AdapterView to avoid + // generation of such event for each child. View selectedView = getSelectedView(); if (selectedView != null) { - populated = selectedView.dispatchPopulateAccessibilityEvent(event); + return selectedView.dispatchPopulateAccessibilityEvent(event); } - if (!populated) { - if (selectedView != null) { - event.setEnabled(selectedView.isEnabled()); - } - event.setItemCount(getCount()); - event.setCurrentItemIndex(getSelectedItemPosition()); - } + return false; + } - return populated; + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + View selectedView = getSelectedView(); + if (selectedView != null) { + event.setEnabled(selectedView.isEnabled()); + } + event.setItemCount(getCount()); + event.setCurrentItemIndex(getSelectedItemPosition()); } @Override diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index 072992e..c773527 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -79,7 +79,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> /** * Map of the children of the {@link AdapterViewAnimator}. */ - HashMap<Integer, ViewAndIndex> mViewsMap = new HashMap<Integer, ViewAndIndex>(); + HashMap<Integer, ViewAndMetaData> mViewsMap = new HashMap<Integer, ViewAndMetaData>(); /** * List of views pending removal from the {@link AdapterViewAnimator} @@ -103,11 +103,6 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> int mCurrentWindowStartUnbounded = 0; /** - * Handler to post events to the main thread - */ - Handler mMainQueue; - - /** * Listens for data changes from the adapter */ AdapterDataSetObserver mDataSetObserver; @@ -163,15 +158,18 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> private static final int DEFAULT_ANIMATION_DURATION = 200; public AdapterViewAnimator(Context context) { - super(context); - initViewAnimator(); + this(context, null); } public AdapterViewAnimator(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public AdapterViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AdapterViewAnimator); + com.android.internal.R.styleable.AdapterViewAnimator, defStyleAttr, 0); int resource = a.getResourceId( com.android.internal.R.styleable.AdapterViewAnimator_inAnimation, 0); if (resource > 0) { @@ -203,17 +201,21 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> * Initialize this {@link AdapterViewAnimator} */ private void initViewAnimator() { - mMainQueue = new Handler(Looper.myLooper()); mPreviousViews = new ArrayList<Integer>(); } - class ViewAndIndex { - ViewAndIndex(View v, int i) { - view = v; - index = i; - } + class ViewAndMetaData { View view; - int index; + int relativeIndex; + int adapterPosition; + long itemId; + + ViewAndMetaData(View view, int relativeIndex, int adapterPosition, long itemId) { + this.view = view; + this.relativeIndex = relativeIndex; + this.adapterPosition = adapterPosition; + this.itemId = itemId; + } } /** @@ -379,6 +381,15 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> } } + private ViewAndMetaData getMetaDataForChild(View child) { + for (ViewAndMetaData vm: mViewsMap.values()) { + if (vm.view == child) { + return vm; + } + } + return null; + } + LayoutParams createOrReuseLayoutParams(View v) { final ViewGroup.LayoutParams currentLp = v.getLayoutParams(); if (currentLp instanceof ViewGroup.LayoutParams) { @@ -481,7 +492,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> if (remove) { View previousView = mViewsMap.get(index).view; - int oldRelativeIndex = mViewsMap.get(index).index; + int oldRelativeIndex = mViewsMap.get(index).relativeIndex; mPreviousViews.add(index); transformViewForTransition(oldRelativeIndex, -1, previousView, animate); @@ -497,7 +508,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> int index = modulo(i, getWindowSize()); int oldRelativeIndex; if (mViewsMap.containsKey(index)) { - oldRelativeIndex = mViewsMap.get(index).index; + oldRelativeIndex = mViewsMap.get(index).relativeIndex; } else { oldRelativeIndex = -1; } @@ -510,14 +521,16 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> if (inOldRange) { View view = mViewsMap.get(index).view; - mViewsMap.get(index).index = newRelativeIndex; + mViewsMap.get(index).relativeIndex = newRelativeIndex; applyTransformForChildAtIndex(view, newRelativeIndex); transformViewForTransition(oldRelativeIndex, newRelativeIndex, view, animate); // Otherwise this view is new to the window } else { // Get the new view from the adapter, add it and apply any transform / animation - View newView = mAdapter.getView(modulo(i, adapterCount), null, this); + final int adapterPosition = modulo(i, adapterCount); + View newView = mAdapter.getView(adapterPosition, null, this); + long itemId = mAdapter.getItemId(adapterPosition); // We wrap the new view in a FrameLayout so as to respect the contract // with the adapter, that is, that we don't modify this view directly @@ -527,7 +540,8 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> if (newView != null) { fl.addView(newView); } - mViewsMap.put(index, new ViewAndIndex(fl, newRelativeIndex)); + mViewsMap.put(index, new ViewAndMetaData(fl, newRelativeIndex, + adapterPosition, itemId)); addChild(fl); applyTransformForChildAtIndex(fl, newRelativeIndex); transformViewForTransition(-1, newRelativeIndex, fl, animate); @@ -604,6 +618,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> case MotionEvent.ACTION_UP: { if (mTouchMode == TOUCH_MODE_DOWN_IN_CURRENT_VIEW) { final View v = getCurrentView(); + final ViewAndMetaData viewData = getMetaDataForChild(v); if (v != null) { if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) { final Handler handler = getHandler(); @@ -616,7 +631,12 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> hideTapFeedback(v); post(new Runnable() { public void run() { - performItemClick(v, 0, 0); + if (viewData != null) { + performItemClick(v, viewData.adapterPosition, + viewData.itemId); + } else { + performItemClick(v, 0, 0); + } } }); } diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index bf63607..8d4aaea 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -199,11 +199,8 @@ public class CheckedTextView extends TextView implements Checkable { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = super.dispatchPopulateAccessibilityEvent(event); - if (!populated) { - event.setChecked(mChecked); - } - return populated; + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setChecked(mChecked); } } diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index 0df45cc..a730018 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -208,22 +208,9 @@ public abstract class CompoundButton extends Button implements Checkable { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = super.dispatchPopulateAccessibilityEvent(event); - - if (!populated) { - int resourceId = 0; - if (mChecked) { - resourceId = R.string.accessibility_compound_button_selected; - } else { - resourceId = R.string.accessibility_compound_button_unselected; - } - String state = getResources().getString(resourceId); - event.getText().add(state); - event.setChecked(mChecked); - } - - return populated; + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setChecked(mChecked); } @Override diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java index 516162a..6c4c39d 100644 --- a/core/java/android/widget/CursorAdapter.java +++ b/core/java/android/widget/CursorAdapter.java @@ -21,7 +21,6 @@ import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; -import android.util.Config; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -440,7 +439,7 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, */ protected void onContentChanged() { if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { - if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); + if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); mDataValid = mCursor.requery(); } } diff --git a/core/java/android/widget/CursorTreeAdapter.java b/core/java/android/widget/CursorTreeAdapter.java index 3fadf4c..44d1656 100644 --- a/core/java/android/widget/CursorTreeAdapter.java +++ b/core/java/android/widget/CursorTreeAdapter.java @@ -22,7 +22,6 @@ import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; -import android.util.Config; import android.util.Log; import android.util.SparseArray; import android.view.View; @@ -499,7 +498,7 @@ public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implem @Override public void onChange(boolean selfChange) { if (mAutoRequery && mCursor != null) { - if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + + if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); mDataValid = mCursor.requery(); } diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index 1d442db..30fb927 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -353,13 +353,14 @@ public class DatePicker extends FrameLayout { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + + final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_YEAR; String selectedDateUtterance = DateUtils.formatDateTime(mContext, mCurrentDate.getTimeInMillis(), flags); event.getText().add(selectedDateUtterance); - return true; } /** @@ -410,74 +411,28 @@ public class DatePicker extends FrameLayout { } /** - * Reorders the spinners according to the date format in the current - * {@link Locale}. + * Reorders the spinners according to the date format that is + * explicitly set by the user and if no such is set fall back + * to the current locale's default format. */ private void reorderSpinners() { - java.text.DateFormat format; - String order; - - /* - * If the user is in a locale where the medium date format is still - * numeric (Japanese and Czech, for example), respect the date format - * order setting. Otherwise, use the order that the locale says is - * appropriate for a spelled-out date. - */ - - if (getShortMonths()[0].startsWith("1")) { - format = DateFormat.getDateFormat(getContext()); - } else { - format = DateFormat.getMediumDateFormat(getContext()); - } - - if (format instanceof SimpleDateFormat) { - order = ((SimpleDateFormat) format).toPattern(); - } else { - // Shouldn't happen, but just in case. - order = new String(DateFormat.getDateFormatOrder(getContext())); - } - - /* - * Remove the 3 spinners from their parent and then add them back in the - * required order. - */ - LinearLayout parent = mSpinners; - parent.removeAllViews(); - - boolean quoted = false; - boolean didDay = false, didMonth = false, didYear = false; - - for (int i = 0; i < order.length(); i++) { - char c = order.charAt(i); - - if (c == '\'') { - quoted = !quoted; - } - - if (!quoted) { - if (c == DateFormat.DATE && !didDay) { - parent.addView(mDaySpinner); - didDay = true; - } else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) { - parent.addView(mMonthSpinner); - didMonth = true; - } else if (c == DateFormat.YEAR && !didYear) { - parent.addView(mYearSpinner); - didYear = true; - } + mSpinners.removeAllViews(); + char[] order = DateFormat.getDateFormatOrder(getContext()); + for (int i = 0; i < order.length; i++) { + switch (order[i]) { + case DateFormat.DATE: + mSpinners.addView(mDaySpinner); + break; + case DateFormat.MONTH: + mSpinners.addView(mMonthSpinner); + break; + case DateFormat.YEAR: + mSpinners.addView(mYearSpinner); + break; + default: + throw new IllegalArgumentException(); } } - - // Shouldn't happen, but just in case. - if (!didMonth) { - parent.addView(mMonthSpinner); - } - if (!didDay) { - parent.addView(mDaySpinner); - } - if (!didYear) { - parent.addView(mYearSpinner); - } } /** diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index f862368..ead9b4f 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -599,12 +599,35 @@ public class ExpandableListView extends ListView { * was already expanded, this will return false) */ public boolean expandGroup(int groupPos) { - boolean retValue = mConnector.expandGroup(groupPos); + return expandGroup(groupPos, false); + } + + /** + * Expand a group in the grouped list view + * + * @param groupPos the group to be expanded + * @param animate true if the expanding group should be animated in + * @return True if the group was expanded, false otherwise (if the group + * was already expanded, this will return false) + */ + public boolean expandGroup(int groupPos, boolean animate) { + PositionMetadata pm = mConnector.getFlattenedPos(ExpandableListPosition.obtain( + ExpandableListPosition.GROUP, groupPos, -1, -1)); + boolean retValue = mConnector.expandGroup(pm); if (mOnGroupExpandListener != null) { mOnGroupExpandListener.onGroupExpand(groupPos); } - + + if (animate) { + final int groupFlatPos = pm.position.flatListPos; + + final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount(); + smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos), + shiftedGroupPosition); + } + pm.recycle(); + return retValue; } diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index f659ead..0659063 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -39,7 +39,7 @@ import java.util.ArrayList; * Children are drawn in a stack, with the most recently added child on top. * The size of the frame layout is the size of its largest child (plus padding), visible * or not (if the FrameLayout's parent permits). Views that are GONE are used for sizing - * only if {@link #setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring()} + * only if {@link #setMeasureAllChildren(boolean) setMeasureAllChildren()} * is set to true. * * @attr ref android.R.styleable#FrameLayout_foreground @@ -485,6 +485,11 @@ public class FrameLayout extends ViewGroup { return new FrameLayout.LayoutParams(getContext(), attrs); } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * {@inheritDoc} */ @@ -566,4 +571,3 @@ public class FrameLayout extends ViewGroup { } } } - diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 1fe6f4b..d8068f9 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -218,15 +218,16 @@ public class ImageView extends View { /** * An optional argument to supply a maximum width for this view. Only valid if - * {@link #setAdjustViewBounds} has been set to true. To set an image to be a maximum of 100 x - * 100 while preserving the original aspect ratio, do the following: 1) set adjustViewBounds to - * true 2) set maxWidth and maxHeight to 100 3) set the height and width layout params to - * WRAP_CONTENT. + * {@link #setAdjustViewBounds(boolean)} has been set to true. To set an image to be a maximum + * of 100 x 100 while preserving the original aspect ratio, do the following: 1) set + * adjustViewBounds to true 2) set maxWidth and maxHeight to 100 3) set the height and width + * layout params to WRAP_CONTENT. * * <p> * Note that this view could be still smaller than 100 x 100 using this approach if the original * image is small. To set an image to a fixed size, specify that size in the layout params and - * then use {@link #setScaleType} to determine how to fit the image within the bounds. + * then use {@link #setScaleType(android.widget.ImageView.ScaleType)} to determine how to fit + * the image within the bounds. * </p> * * @param maxWidth maximum width for this view @@ -240,15 +241,16 @@ public class ImageView extends View { /** * An optional argument to supply a maximum height for this view. Only valid if - * {@link #setAdjustViewBounds} has been set to true. To set an image to be a maximum of 100 x - * 100 while preserving the original aspect ratio, do the following: 1) set adjustViewBounds to - * true 2) set maxWidth and maxHeight to 100 3) set the height and width layout params to - * WRAP_CONTENT. + * {@link #setAdjustViewBounds(boolean)} has been set to true. To set an image to be a + * maximum of 100 x 100 while preserving the original aspect ratio, do the following: 1) set + * adjustViewBounds to true 2) set maxWidth and maxHeight to 100 3) set the height and width + * layout params to WRAP_CONTENT. * * <p> * Note that this view could be still smaller than 100 x 100 using this approach if the original * image is small. To set an image to a fixed size, specify that size in the layout params and - * then use {@link #setScaleType} to determine how to fit the image within the bounds. + * then use {@link #setScaleType(android.widget.ImageView.ScaleType)} to determine how to fit + * the image within the bounds. * </p> * * @param maxHeight maximum height for this view @@ -272,8 +274,8 @@ public class ImageView extends View { * * <p class="note">This does Bitmap reading and decoding on the UI * thread, which can cause a latency hiccup. If that's a concern, - * consider using {@link #setImageDrawable} or - * {@link #setImageBitmap} and + * consider using {@link #setImageDrawable(android.graphics.drawable.Drawable)} or + * {@link #setImageBitmap(android.graphics.Bitmap)} and * {@link android.graphics.BitmapFactory} instead.</p> * * @param resId the resource identifier of the the drawable @@ -297,8 +299,8 @@ public class ImageView extends View { * * <p class="note">This does Bitmap reading and decoding on the UI * thread, which can cause a latency hiccup. If that's a concern, - * consider using {@link #setImageDrawable} or - * {@link #setImageBitmap} and + * consider using {@link #setImageDrawable(android.graphics.drawable.Drawable)} or + * {@link #setImageBitmap(android.graphics.Bitmap)} and * {@link android.graphics.BitmapFactory} instead.</p> * * @param uri The Uri of an image @@ -902,12 +904,12 @@ public class ImageView extends View { /** * <p>Set the offset of the widget's text baseline from the widget's top - * boundary. This value is overridden by the {@link #setBaselineAlignBottom} + * boundary. This value is overridden by the {@link #setBaselineAlignBottom(boolean)} * property.</p> * * @param baseline The baseline to use, or -1 if none is to be provided. * - * @see #setBaseline + * @see #setBaseline(int) * @attr ref android.R.styleable#ImageView_baseline */ public void setBaseline(int baseline) { diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index fd0e53d..86fefaf 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -201,6 +201,11 @@ public class LinearLayout extends ViewGroup { mShowDividers = showDividers; } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * @return A flag set indicating how dividers should be shown around items. * @see #setShowDividers(int) @@ -230,6 +235,39 @@ public class LinearLayout extends ViewGroup { requestLayout(); } + /** + * Set padding displayed on both ends of dividers. + * + * @param padding Padding value in pixels that will be applied to each end + * + * @see #setShowDividers(int) + * @see #setDividerDrawable(Drawable) + * @see #getDividerPadding() + */ + public void setDividerPadding(int padding) { + mDividerPadding = padding; + } + + /** + * Get the padding size used to inset dividers in pixels + * + * @see #setShowDividers(int) + * @see #setDividerDrawable(Drawable) + * @see #setDividerPadding(int) + */ + public int getDividerPadding() { + return mDividerPadding; + } + + /** + * Get the width of the current divider drawable. + * + * @hide Used internally by framework. + */ + public int getDividerWidth() { + return mDividerWidth; + } + @Override protected void onDraw(Canvas canvas) { if (mDivider == null) { @@ -244,29 +282,15 @@ public class LinearLayout extends ViewGroup { } void drawDividersVertical(Canvas canvas) { - final boolean showDividerBeginning = - (mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING; - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - final boolean showDividerEnd = - (mShowDividers & SHOW_DIVIDER_END) == SHOW_DIVIDER_END; - final int count = getVirtualChildCount(); int top = getPaddingTop(); - boolean firstVisible = true; for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { top += measureNullChild(i); } else if (child.getVisibility() != GONE) { - if (firstVisible) { - firstVisible = false; - if (showDividerBeginning) { - drawHorizontalDivider(canvas, top); - top += mDividerHeight; - } - } else if (showDividerMiddle) { + if (hasDividerBeforeChildAt(i)) { drawHorizontalDivider(canvas, top); top += mDividerHeight; } @@ -276,35 +300,21 @@ public class LinearLayout extends ViewGroup { } } - if (showDividerEnd) { + if (hasDividerBeforeChildAt(count)) { drawHorizontalDivider(canvas, top); } } void drawDividersHorizontal(Canvas canvas) { - final boolean showDividerBeginning = - (mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING; - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - final boolean showDividerEnd = - (mShowDividers & SHOW_DIVIDER_END) == SHOW_DIVIDER_END; - final int count = getVirtualChildCount(); int left = getPaddingLeft(); - boolean firstVisible = true; for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { left += measureNullChild(i); } else if (child.getVisibility() != GONE) { - if (firstVisible) { - firstVisible = false; - if (showDividerBeginning) { - drawVerticalDivider(canvas, left); - left += mDividerWidth; - } - } else if (showDividerMiddle) { + if (hasDividerBeforeChildAt(i)) { drawVerticalDivider(canvas, left); left += mDividerWidth; } @@ -314,7 +324,7 @@ public class LinearLayout extends ViewGroup { } } - if (showDividerEnd) { + if (hasDividerBeforeChildAt(count)) { drawVerticalDivider(canvas, left); } } @@ -523,6 +533,23 @@ public class LinearLayout extends ViewGroup { } /** + * Determines where to position dividers between children. + * + * @param childIndex Index of child to check for preceding divider + * @return true if there should be a divider before the child at childIndex + * @hide Pending API consideration. Currently only used internally by the system. + */ + protected boolean hasDividerBeforeChildAt(int childIndex) { + if (childIndex == 0) { + return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0; + } else if (childIndex == getChildCount()) { + return (mShowDividers & SHOW_DIVIDER_END) != 0; + } else { + return (mShowDividers & SHOW_DIVIDER_MIDDLE) != 0; + } + } + + /** * Measures the children when the orientation of this LinearLayout is set * to {@link #VERTICAL}. * @@ -554,14 +581,7 @@ public class LinearLayout extends ViewGroup { int largestChildHeight = Integer.MIN_VALUE; - // A divider at the end will change how much space views can consume. - final boolean showDividerBeginning = - (mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING; - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - // See how tall everyone is. Also remember max width. - boolean firstVisible = true; for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); @@ -575,12 +595,7 @@ public class LinearLayout extends ViewGroup { continue; } - if (firstVisible) { - firstVisible = false; - if (showDividerBeginning) { - mTotalLength += mDividerHeight; - } - } else if (showDividerMiddle) { + if (hasDividerBeforeChildAt(i)) { mTotalLength += mDividerHeight; } @@ -677,7 +692,7 @@ public class LinearLayout extends ViewGroup { i += getChildrenSkipCount(child, i); } - if (mTotalLength > 0 && (mShowDividers & SHOW_DIVIDER_END) == SHOW_DIVIDER_END) { + if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) { mTotalLength += mDividerHeight; } @@ -881,14 +896,7 @@ public class LinearLayout extends ViewGroup { int largestChildWidth = Integer.MIN_VALUE; - // A divider at the end will change how much space views can consume. - final boolean showDividerBeginning = - (mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING; - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - // See how wide everyone is. Also remember max height. - boolean firstVisible = true; for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); @@ -902,12 +910,7 @@ public class LinearLayout extends ViewGroup { continue; } - if (firstVisible) { - firstVisible = false; - if (showDividerBeginning) { - mTotalLength += mDividerWidth; - } - } else if (showDividerMiddle) { + if (hasDividerBeforeChildAt(i)) { mTotalLength += mDividerWidth; } @@ -1022,7 +1025,7 @@ public class LinearLayout extends ViewGroup { i += getChildrenSkipCount(child, i); } - if (mTotalLength > 0 && (mShowDividers & SHOW_DIVIDER_END) == SHOW_DIVIDER_END) { + if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) { mTotalLength += mDividerWidth; } @@ -1358,13 +1361,6 @@ public class LinearLayout extends ViewGroup { } - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - - if ((mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING) { - childTop += mDividerHeight; - } - for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { @@ -1399,15 +1395,15 @@ public class LinearLayout extends ViewGroup { break; } + if (hasDividerBeforeChildAt(i)) { + childTop += mDividerHeight; + } + childTop += lp.topMargin; setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); - if (showDividerMiddle) { - childTop += mDividerHeight; - } - i += getChildrenSkipCount(child, i); } } @@ -1458,13 +1454,6 @@ public class LinearLayout extends ViewGroup { } } - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - - if ((mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING) { - childLeft += mDividerWidth; - } - for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); @@ -1523,16 +1512,16 @@ public class LinearLayout extends ViewGroup { break; } + if (hasDividerBeforeChildAt(i)) { + childLeft += mDividerWidth; + } + childLeft += lp.leftMargin; setChildFrame(child, childLeft + getLocationOffset(child), childTop, childWidth, childHeight); childLeft += childWidth + lp.rightMargin + getNextLocationOffset(child); - if (showDividerMiddle) { - childLeft += mDividerWidth; - } - i += getChildrenSkipCount(child, i); } } diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index af954c9..e7a9e41 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -251,7 +251,7 @@ public class ListView extends AbsListView { */ public void addHeaderView(View v, Object data, boolean isSelectable) { - if (mAdapter != null) { + if (mAdapter != null && ! (mAdapter instanceof HeaderViewListAdapter)) { throw new IllegalStateException( "Cannot add header view to list -- setAdapter has already been called."); } @@ -261,6 +261,12 @@ public class ListView extends AbsListView { info.data = data; info.isSelectable = isSelectable; mHeaderViewInfos.add(info); + + // in the case of re-adding a header view, or adding one later on, + // we need to notify the observer + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } } /** @@ -294,7 +300,9 @@ public class ListView extends AbsListView { if (mHeaderViewInfos.size() > 0) { boolean result = false; if (((HeaderViewListAdapter) mAdapter).removeHeader(v)) { - mDataSetObserver.onChanged(); + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } result = true; } removeFixedViewInfo(v, mHeaderViewInfos); @@ -328,6 +336,12 @@ public class ListView extends AbsListView { * @param isSelectable true if the footer view can be selected */ public void addFooterView(View v, Object data, boolean isSelectable) { + + // NOTE: do not enforce the adapter being null here, since unlike in + // addHeaderView, it was never enforced here, and so existing apps are + // relying on being able to add a footer and then calling setAdapter to + // force creation of the HeaderViewListAdapter wrapper + FixedViewInfo info = new FixedViewInfo(); info.view = v; info.data = data; @@ -371,7 +385,9 @@ public class ListView extends AbsListView { if (mFooterViewInfos.size() > 0) { boolean result = false; if (((HeaderViewListAdapter) mAdapter).removeFooter(v)) { - mDataSetObserver.onChanged(); + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } result = true; } removeFixedViewInfo(v, mFooterViewInfos); @@ -1552,7 +1568,7 @@ public class ListView extends AbsListView { // take focus back to us temporarily to avoid the eventual // call to clear focus when removing the focused child below - // from messing things up when ViewRoot assigns focus back + // from messing things up when ViewAncestor assigns focus back // to someone else final View focusedChild = getFocusedChild(); if (focusedChild != null) { @@ -1982,36 +1998,28 @@ public class ListView extends AbsListView { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = super.dispatchPopulateAccessibilityEvent(event); + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); // If the item count is less than 15 then subtract disabled items from the count and // position. Otherwise ignore disabled items. - if (!populated) { - int itemCount = 0; - int currentItemIndex = getSelectedItemPosition(); - - ListAdapter adapter = getAdapter(); - if (adapter != null) { - final int count = adapter.getCount(); - if (count < 15) { - for (int i = 0; i < count; i++) { - if (adapter.isEnabled(i)) { - itemCount++; - } else if (i <= currentItemIndex) { - currentItemIndex--; - } - } - } else { - itemCount = count; + int itemCount = 0; + int currentItemIndex = getSelectedItemPosition(); + + ListAdapter adapter = getAdapter(); + if (adapter != null) { + final int count = adapter.getCount(); + for (int i = 0; i < count; i++) { + if (adapter.isEnabled(i)) { + itemCount++; + } else if (i <= currentItemIndex) { + currentItemIndex--; } } - - event.setItemCount(itemCount); - event.setCurrentItemIndex(currentItemIndex); } - return populated; + event.setItemCount(itemCount); + event.setCurrentItemIndex(currentItemIndex); } /** diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java index 02c1ec7..134e4c4 100644 --- a/core/java/android/widget/MultiAutoCompleteTextView.java +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -30,7 +30,7 @@ import android.widget.MultiAutoCompleteTextView.Tokenizer; * can show completion suggestions for the substring of the text where * the user is typing instead of necessarily for the entire thing. * <p> - * You must must provide a {@link Tokenizer} to distinguish the + * You must provide a {@link Tokenizer} to distinguish the * various substrings. * * <p>The following code snippet shows how to create a text view which suggests @@ -41,7 +41,7 @@ import android.widget.MultiAutoCompleteTextView.Tokenizer; * protected void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); * setContentView(R.layout.autocomplete_7); - * + * * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, * android.R.layout.simple_dropdown_item_1line, COUNTRIES); * MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit); @@ -132,7 +132,7 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { * Instead of validating the entire text, this subclass method validates * each token of the text individually. Empty tokens are removed. */ - @Override + @Override public void performValidation() { Validator v = getValidator(); diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index de32c2b..1e1a043 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -392,11 +392,11 @@ public class PopupWindow { mContentView = contentView; - if (mContext == null) { + if (mContext == null && mContentView != null) { mContext = mContentView.getContext(); } - if (mWindowManager == null) { + if (mWindowManager == null && mContentView != null) { mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } } @@ -939,7 +939,9 @@ public class PopupWindow { * @param p the layout parameters of the popup's content view */ private void invokePopup(WindowManager.LayoutParams p) { - p.packageName = mContext.getPackageName(); + if (mContext != null) { + p.packageName = mContext.getPackageName(); + } mWindowManager.addView(mPopupView, p); } diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index 6b676b4..30374af 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -16,6 +16,8 @@ package android.widget; +import com.android.internal.R; + import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -41,6 +43,8 @@ import android.view.Gravity; import android.view.RemotableViewMethod; import android.view.View; import android.view.ViewDebug; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -49,8 +53,6 @@ import android.view.animation.LinearInterpolator; import android.view.animation.Transformation; import android.widget.RemoteViews.RemoteView; -import com.android.internal.R; - /** * <p> @@ -187,6 +189,7 @@ import com.android.internal.R; public class ProgressBar extends View { private static final int MAX_LEVEL = 10000; private static final int ANIMATION_RESOLUTION = 200; + private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200; int mMinWidth; int mMaxWidth; @@ -218,6 +221,8 @@ public class ProgressBar extends View { private int mAnimationResolution; + private AccessibilityEventSender mAccessibilityEventSender; + /** * Create a new progress bar with range 0...100 and initial progress of 0. * @param context the application environment @@ -604,8 +609,11 @@ public class ProgressBar extends View { onProgressRefresh(scale, fromUser); } } - - void onProgressRefresh(float scale, boolean fromUser) { + + void onProgressRefresh(float scale, boolean fromUser) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + scheduleAccessibilityEventSender(); + } } private synchronized void refreshProgress(int id, int progress, boolean fromUser) { @@ -1069,8 +1077,46 @@ public class ProgressBar extends View { if (mIndeterminate) { stopAnimation(); } + if(mRefreshProgressRunnable != null) { + removeCallbacks(mRefreshProgressRunnable); + } + if (mAccessibilityEventSender != null) { + removeCallbacks(mAccessibilityEventSender); + } // This should come after stopAnimation(), otherwise an invalidate message remains in the // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation super.onDetachedFromWindow(); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setItemCount(mMax); + event.setCurrentItemIndex(mProgress); + } + + /** + * Schedule a command for sending an accessibility event. + * </br> + * Note: A command is used to ensure that accessibility events + * are sent at most one in a given time frame to save + * system resources while the progress changes quickly. + */ + private void scheduleAccessibilityEventSender() { + if (mAccessibilityEventSender == null) { + mAccessibilityEventSender = new AccessibilityEventSender(); + } else { + removeCallbacks(mAccessibilityEventSender); + } + postDelayed(mAccessibilityEventSender, TIMEOUT_SEND_ACCESSIBILITY_EVENT); + } + + /** + * Command for sending an accessibility event. + */ + private class AccessibilityEventSender implements Runnable { + public void run() { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + } } diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index a47359f..9069283 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -186,6 +186,11 @@ public class RelativeLayout extends ViewGroup { a.recycle(); } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * Defines which View is ignored when the gravity is applied. This setting has no * effect if the gravity is <code>Gravity.LEFT | Gravity.TOP</code>. diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index c854fac..9cf2718 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -125,7 +125,7 @@ public class RemoteViews implements Parcelable, Filter { * SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!! */ private abstract static class Action implements Parcelable { - public abstract void apply(View root) throws ActionException; + public abstract void apply(View root, ViewGroup rootParent) throws ActionException; public int describeContents() { return 0; @@ -183,7 +183,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View view = root.findViewById(viewId); if (!(view instanceof AdapterView<?>)) return; @@ -214,7 +214,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -295,7 +295,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -360,6 +360,60 @@ public class RemoteViews implements Parcelable, Filter { public final static int TAG = 8; } + private class SetRemoteViewsAdapterIntent extends Action { + public SetRemoteViewsAdapterIntent(int id, Intent intent) { + this.viewId = id; + this.intent = intent; + } + + public SetRemoteViewsAdapterIntent(Parcel parcel) { + viewId = parcel.readInt(); + intent = Intent.CREATOR.createFromParcel(parcel); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + intent.writeToParcel(dest, flags); + } + + @Override + public void apply(View root, ViewGroup rootParent) { + final View target = root.findViewById(viewId); + if (target == null) return; + + // Ensure that we are applying to an AppWidget root + if (!(rootParent instanceof AppWidgetHostView)) { + Log.e("RemoteViews", "SetRemoteViewsAdapterIntent action can only be used for " + + "AppWidgets (root id: " + viewId + ")"); + return; + } + // Ensure that we are calling setRemoteAdapter on an AdapterView that supports it + if (!(target instanceof AbsListView) && !(target instanceof AdapterViewAnimator)) { + Log.e("RemoteViews", "Cannot setRemoteViewsAdapter on a view which is not " + + "an AbsListView or AdapterViewAnimator (id: " + viewId + ")"); + return; + } + + // Embed the AppWidget Id for use in RemoteViewsAdapter when connecting to the intent + // RemoteViewsService + AppWidgetHostView host = (AppWidgetHostView) rootParent; + intent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, host.getAppWidgetId()); + if (target instanceof AbsListView) { + AbsListView v = (AbsListView) target; + v.setRemoteViewsAdapter(intent); + } else if (target instanceof AdapterViewAnimator) { + AdapterViewAnimator v = (AdapterViewAnimator) target; + v.setRemoteViewsAdapter(intent); + } + } + + int viewId; + Intent intent; + + public final static int TAG = 10; + } + /** * Equivalent to calling * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} @@ -383,7 +437,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -479,7 +533,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -539,7 +593,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View view = root.findViewById(viewId); if (view == null) return; @@ -755,7 +809,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View view = root.findViewById(viewId); if (view == null) return; @@ -850,7 +904,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final Context context = root.getContext(); final ViewGroup target = (ViewGroup) root.findViewById(viewId); if (target == null) return; @@ -952,6 +1006,9 @@ public class RemoteViews implements Parcelable, Filter { case SetOnClickFillInIntent.TAG: mActions.add(new SetOnClickFillInIntent(parcel)); break; + case SetRemoteViewsAdapterIntent.TAG: + mActions.add(new SetRemoteViewsAdapterIntent(parcel)); + break; default: throw new ActionException("Tag " + tag + " not found"); } @@ -1287,16 +1344,29 @@ public class RemoteViews implements Parcelable, Filter { /** * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}. * - * @param appWidgetId The id of the app widget which contains the specified view + * @param appWidgetId The id of the app widget which contains the specified view. (This + * parameter is ignored in this deprecated method) * @param viewId The id of the view whose text should change * @param intent The intent of the service which will be * providing data to the RemoteViewsAdapter + * @deprecated This method has been deprecated. See + * {@link android.widget.RemoteViews#setRemoteAdapter(int, Intent)} */ + @Deprecated public void setRemoteAdapter(int appWidgetId, int viewId, Intent intent) { - // Embed the AppWidget Id for use in RemoteViewsAdapter when connecting to the intent - // RemoteViewsService - intent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, appWidgetId); - setIntent(viewId, "setRemoteViewsAdapter", intent); + setRemoteAdapter(viewId, intent); + } + + /** + * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}. + * Can only be used for App Widgets. + * + * @param viewId The id of the view whose text should change + * @param intent The intent of the service which will be + * providing data to the RemoteViewsAdapter + */ + public void setRemoteAdapter(int viewId, Intent intent) { + addAction(new SetRemoteViewsAdapterIntent(viewId, intent)); } /** @@ -1499,7 +1569,7 @@ public class RemoteViews implements Parcelable, Filter { result = inflater.inflate(mLayoutId, parent, false); - performApply(result); + performApply(result, parent); return result; } @@ -1514,15 +1584,15 @@ public class RemoteViews implements Parcelable, Filter { */ public void reapply(Context context, View v) { prepareContext(context); - performApply(v); + performApply(v, (ViewGroup) v.getParent()); } - private void performApply(View v) { + private void performApply(View v, ViewGroup parent) { if (mActions != null) { final int count = mActions.size(); for (int i = 0; i < count; i++) { Action a = mActions.get(i); - a.apply(v); + a.apply(v, parent); } } } diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 1c0a2bb..40b0a9c 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -29,6 +29,7 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.RemoteException; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -156,13 +157,16 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // create in response to this bind factory.onDataSetChanged(); } - } catch (Exception e) { + } catch (RemoteException e) { Log.e(TAG, "Error notifying factory of data set changed in " + "onServiceConnected(): " + e.getMessage()); // Return early to prevent anything further from being notified // (effectively nothing has changed) return; + } catch (RuntimeException e) { + Log.e(TAG, "Error notifying factory of data set changed in " + + "onServiceConnected(): " + e.getMessage()); } // Request meta data so that we have up to date data when calling back to @@ -777,7 +781,9 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback tmpMetaData.count = count; tmpMetaData.setLoadingViewTemplates(loadingView, firstView); } - } catch (Exception e) { + } catch(RemoteException e) { + processException("updateMetaData", e); + } catch(RuntimeException e) { processException("updateMetaData", e); } } @@ -792,12 +798,15 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback try { remoteViews = factory.getViewAt(position); itemId = factory.getItemId(position); - } catch (Exception e) { + } catch (RemoteException e) { Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage()); // Return early to prevent additional work in re-centering the view cache, and // swapping from the loading view return; + } catch (RuntimeException e) { + Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage()); + return; } if (remoteViews == null) { @@ -971,18 +980,20 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback return getCount() <= 0; } - private void onNotifyDataSetChanged() { // Complete the actual notifyDataSetChanged() call initiated earlier IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); try { factory.onDataSetChanged(); - } catch (Exception e) { + } catch (RemoteException e) { Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); // Return early to prevent from further being notified (since nothing has // changed) return; + } catch (RuntimeException e) { + Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); + return; } // Flush the cache so that we can reload new items from the service diff --git a/core/java/android/widget/RemoteViewsService.java b/core/java/android/widget/RemoteViewsService.java index e0b08d4..7ba4777 100644 --- a/core/java/android/widget/RemoteViewsService.java +++ b/core/java/android/widget/RemoteViewsService.java @@ -138,34 +138,87 @@ public abstract class RemoteViewsService extends Service { return mIsCreated; } public synchronized void onDataSetChanged() { - mFactory.onDataSetChanged(); + try { + mFactory.onDataSetChanged(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } } public synchronized int getCount() { - return mFactory.getCount(); + int count = 0; + try { + count = mFactory.getCount(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return count; } public synchronized RemoteViews getViewAt(int position) { - RemoteViews rv = mFactory.getViewAt(position); - rv.setIsWidgetCollectionChild(true); + RemoteViews rv = null; + try { + rv = mFactory.getViewAt(position); + if (rv != null) { + rv.setIsWidgetCollectionChild(true); + } + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } return rv; } public synchronized RemoteViews getLoadingView() { - return mFactory.getLoadingView(); + RemoteViews rv = null; + try { + rv = mFactory.getLoadingView(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return rv; } public synchronized int getViewTypeCount() { - return mFactory.getViewTypeCount(); + int count = 0; + try { + count = mFactory.getViewTypeCount(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return count; } public synchronized long getItemId(int position) { - return mFactory.getItemId(position); + long id = 0; + try { + id = mFactory.getItemId(position); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return id; } public synchronized boolean hasStableIds() { - return mFactory.hasStableIds(); + boolean hasStableIds = false; + try { + hasStableIds = mFactory.hasStableIds(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return hasStableIds; } public void onDestroy(Intent intent) { synchronized (sLock) { Intent.FilterComparison fc = new Intent.FilterComparison(intent); if (RemoteViewsService.sRemoteViewFactories.containsKey(fc)) { RemoteViewsFactory factory = RemoteViewsService.sRemoteViewFactories.get(fc); - factory.onDestroy(); + try { + factory.onDestroy(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } RemoteViewsService.sRemoteViewFactories.remove(fc); } } diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index ade3a0a..27edb88 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -162,6 +162,11 @@ public class ScrollView extends FrameLayout { } @Override + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override protected float getTopFadingEdgeStrength() { if (getChildCount() == 0) { return 0.0f; diff --git a/core/java/android/widget/SimpleCursorAdapter.java b/core/java/android/widget/SimpleCursorAdapter.java index 3d2a252..c5c6c69 100644 --- a/core/java/android/widget/SimpleCursorAdapter.java +++ b/core/java/android/widget/SimpleCursorAdapter.java @@ -338,6 +338,12 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { @Override public Cursor swapCursor(Cursor c) { + // super.swapCursor() will notify observers before we have + // a valid mapping, make sure we have a mapping before this + // happens + if (mFrom == null) { + findColumns(mOriginalFrom); + } Cursor res = super.swapCursor(c); // rescan columns in case cursor layout is different findColumns(mOriginalFrom); @@ -358,7 +364,13 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { public void changeCursorAndColumns(Cursor c, String[] from, int[] to) { mOriginalFrom = from; mTo = to; - super.changeCursor(c); + // super.changeCursor() will notify observers before we have + // a valid mapping, make sure we have a mapping before this + // happens + if (mFrom == null) { + findColumns(mOriginalFrom); + } + super.changeCursor(c); findColumns(mOriginalFrom); } diff --git a/core/java/android/widget/StackView.java b/core/java/android/widget/StackView.java index 21c61bd..71c91e1 100644 --- a/core/java/android/widget/StackView.java +++ b/core/java/android/widget/StackView.java @@ -20,6 +20,7 @@ import java.lang.ref.WeakReference; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BlurMaskFilter; import android.graphics.Canvas; @@ -132,6 +133,8 @@ public class StackView extends AdapterViewAnimator { private int mMaximumVelocity; private VelocityTracker mVelocityTracker; private boolean mTransitionIsSetup = false; + private int mResOutColor; + private int mClickColor; private static HolographicHelper sHolographicHelper; private ImageView mHighlight; @@ -146,12 +149,24 @@ public class StackView extends AdapterViewAnimator { private final Rect stackInvalidateRect = new Rect(); public StackView(Context context) { - super(context); - initStackView(); + this(context, null); } public StackView(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, com.android.internal.R.attr.stackViewStyle); + } + + public StackView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.StackView, defStyleAttr, 0); + + mResOutColor = a.getColor( + com.android.internal.R.styleable.StackView_resOutColor, 0); + mClickColor = a.getColor( + com.android.internal.R.styleable.StackView_clickColor, 0); + + a.recycle(); initStackView(); } @@ -357,7 +372,7 @@ public class StackView extends AdapterViewAnimator { private void setupStackSlider(View v, int mode) { mStackSlider.setMode(mode); if (v != null) { - mHighlight.setImageBitmap(sHolographicHelper.createOutline(v)); + mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor)); mHighlight.setRotation(v.getRotation()); mHighlight.setTranslationY(v.getTranslationY()); mHighlight.setTranslationX(v.getTranslationX()); @@ -412,8 +427,8 @@ public class StackView extends AdapterViewAnimator { // Here we need to make sure that the z-order of the children is correct for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) { int index = modulo(i, getWindowSize()); - ViewAndIndex vi = mViewsMap.get(index); - if (vi != null) { + ViewAndMetaData vm = mViewsMap.get(index); + if (vm != null) { View v = mViewsMap.get(index).view; if (v != null) v.bringToFront(); } @@ -429,8 +444,8 @@ public class StackView extends AdapterViewAnimator { if (!mClickFeedbackIsValid) { View v = getViewAtRelativeIndex(1); if (v != null) { - mClickFeedback.setImageBitmap(sHolographicHelper.createOutline(v, - HolographicHelper.CLICK_FEEDBACK)); + mClickFeedback.setImageBitmap( + sHolographicHelper.createClickOutline(v, mClickColor)); mClickFeedback.setTranslationX(v.getTranslationX()); mClickFeedback.setTranslationY(v.getTranslationY()); } @@ -1355,16 +1370,19 @@ public class StackView extends AdapterViewAnimator { mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL); } - Bitmap createOutline(View v) { - return createOutline(v, RES_OUT); + Bitmap createClickOutline(View v, int color) { + return createOutline(v, CLICK_FEEDBACK, color); + } + + Bitmap createResOutline(View v, int color) { + return createOutline(v, RES_OUT, color); } - Bitmap createOutline(View v, int type) { + Bitmap createOutline(View v, int type, int color) { + mHolographicPaint.setColor(color); if (type == RES_OUT) { - mHolographicPaint.setColor(0xff6699ff); mBlurPaint.setMaskFilter(mSmallBlurMaskFilter); } else if (type == CLICK_FEEDBACK) { - mHolographicPaint.setColor(0x886699ff); mBlurPaint.setMaskFilter(mLargeBlurMaskFilter); } diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java index 6f76dd0..1fe1f79 100644 --- a/core/java/android/widget/TabWidget.java +++ b/core/java/android/widget/TabWidget.java @@ -427,12 +427,19 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - event.setItemCount(getTabCount()); - event.setCurrentItemIndex(mSelectedTab); + onPopulateAccessibilityEvent(event); + // Dispatch only to the selected tab. if (mSelectedTab != -1) { - getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event); + return getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event); } - return true; + return false; + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setItemCount(getTabCount()); + event.setCurrentItemIndex(mSelectedTab); } /** diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 13b9285f..d58c72b 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -60,6 +60,7 @@ import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; import android.text.SpannableString; +import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; import android.text.StaticLayout; @@ -80,12 +81,17 @@ import android.text.method.SingleLineTransformationMethod; import android.text.method.TextKeyListener; import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; +import android.text.method.WordIterator; import android.text.style.ClickableSpan; import android.text.style.ParagraphStyle; +import android.text.style.SuggestionSpan; +import android.text.style.TextAppearanceSpan; import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; import android.text.style.UpdateAppearance; import android.text.util.Linkify; import android.util.AttributeSet; +import android.util.DisplayMetrics; import android.util.FloatMath; import android.util.Log; import android.util.TypedValue; @@ -102,12 +108,12 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewAncestor; import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; -import android.view.ViewRoot; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; @@ -125,6 +131,7 @@ import android.widget.RemoteViews.RemoteView; import java.io.IOException; import java.lang.ref.WeakReference; +import java.text.BreakIterator; import java.util.ArrayList; /** @@ -309,6 +316,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mTextEditPasteWindowLayout, mTextEditSidePasteWindowLayout; private int mTextEditNoPasteWindowLayout, mTextEditSideNoPasteWindowLayout; + private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout; + private int mTextEditSuggestionItemLayout; + private SuggestionsPopupWindow mSuggestionsPopupWindow; + private SuggestionRangeSpan mSuggestionRangeSpan; + private int mCursorDrawableRes; private final Drawable[] mCursorDrawable = new Drawable[2]; private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2 @@ -324,6 +336,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Set when this TextView gained focus with some text selected. Will start selection mode. private boolean mCreatedWithASelection = false; + private WordIterator mWordIterator; + /* * Kick-start the font cache for the zygote process (to pay the cost of * initializing freetype for our default font only once). @@ -777,6 +791,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTextEditSideNoPasteWindowLayout = a.getResourceId(attr, 0); break; + case com.android.internal.R.styleable.TextView_textEditSuggestionsBottomWindowLayout: + mTextEditSuggestionsBottomWindowLayout = a.getResourceId(attr, 0); + break; + + case com.android.internal.R.styleable.TextView_textEditSuggestionsTopWindowLayout: + mTextEditSuggestionsTopWindowLayout = a.getResourceId(attr, 0); + break; + + case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout: + mTextEditSuggestionItemLayout = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.TextView_textIsSelectable: mTextIsSelectable = a.getBoolean(attr, false); break; @@ -2949,6 +2975,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener advancesIndex); } + public float getTextRunAdvances(int start, int end, int contextStart, + int contextEnd, int flags, float[] advances, int advancesIndex, + Paint p, int reserved) { + int count = end - start; + int contextCount = contextEnd - contextStart; + return p.getTextRunAdvances(mChars, start + mStart, count, + contextStart + mStart, contextCount, flags, advances, + advancesIndex, reserved); + } + public int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset, int cursorOpt, Paint p) { int contextCount = contextEnd - contextStart; @@ -3337,13 +3373,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Handler h = getHandler(); if (h != null) { long eventTime = SystemClock.uptimeMillis(); - h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE | KeyEvent.FLAG_EDITOR_ACTION))); - h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, new KeyEvent(SystemClock.uptimeMillis(), eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, @@ -3977,13 +4013,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener observer.removeOnPreDrawListener(this); mPreDrawState = PREDRAW_NOT_REGISTERED; } - // No need to create the controller, as getXXController would. - if (mInsertionPointCursorController != null) { - observer.removeOnTouchModeChangeListener(mInsertionPointCursorController); - } - if (mSelectionModifierCursorController != null) { - observer.removeOnTouchModeChangeListener(mSelectionModifierCursorController); - } if (mError != null) { hideError(); @@ -4210,6 +4239,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override protected void onDraw(Canvas canvas) { + if (mPreDrawState == PREDRAW_DONE) { + final ViewTreeObserver observer = getViewTreeObserver(); + observer.removeOnPreDrawListener(this); + mPreDrawState = PREDRAW_NOT_REGISTERED; + } + if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return; restartMarqueeIfNeeded(); @@ -4281,12 +4316,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - if (mPreDrawState == PREDRAW_DONE) { - final ViewTreeObserver observer = getViewTreeObserver(); - observer.removeOnPreDrawListener(this); - mPreDrawState = PREDRAW_NOT_REGISTERED; - } - int color = mCurTextColor; if (mLayout == null) { @@ -4549,15 +4578,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (translate) canvas.translate(0, -cursorOffsetVertical); } - /** - * Update the positions of the CursorControllers. Needed by WebTextView, - * which does not draw. - * @hide - */ - protected void updateCursorControllerPositions() { - // TODO remove - } - @Override public void getFocusedRect(Rect r) { if (mLayout == null) { @@ -5236,6 +5256,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @param text The auto complete text the user has selected. */ public void onCommitCompletion(CompletionInfo text) { + // intentionally empty } /** @@ -5422,6 +5443,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * of edit operations through a call to link {@link #beginBatchEdit()}. */ public void onBeginBatchEdit() { + // intentionally empty } /** @@ -5429,6 +5451,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * of edit operations through a call to link {@link #endBatchEdit}. */ public void onEndBatchEdit() { + // intentionally empty } /** @@ -6275,15 +6298,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (isFocused()) { - // This offsets because getInterestingRect() is in terms of - // viewport coordinates, but requestRectangleOnScreen() - // is in terms of content coordinates. + // This offsets because getInterestingRect() is in terms of viewport coordinates, but + // requestRectangleOnScreen() is in terms of content coordinates. - Rect r = new Rect(x, top, x + 1, bottom); - getInterestingRect(r, line); - r.offset(mScrollX, mScrollY); + if (mTempRect == null) mTempRect = new Rect(); + mTempRect.set(x, top, x + 1, bottom); + getInterestingRect(mTempRect, line); + mTempRect.offset(mScrollX, mScrollY); - if (requestRectangleOnScreen(r)) { + if (requestRectangleOnScreen(mTempRect)) { changed = true; } } @@ -6756,25 +6779,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * This method is called when the text is changed, in case any - * subclasses would like to know. + * This method is called when the text is changed, in case any subclasses + * would like to know. * - * @param text The text the TextView is displaying. - * @param start The offset of the start of the range of the text - * that was modified. - * @param before The offset of the former end of the range of the - * text that was modified. If text was simply inserted, - * this will be the same as <code>start</code>. - * If text was replaced with new text or deleted, the - * length of the old text was <code>before-start</code>. - * @param after The offset of the end of the range of the text - * that was modified. If text was simply deleted, - * this will be the same as <code>start</code>. - * If text was replaced with new text or inserted, - * the length of the new text is <code>after-start</code>. + * Within <code>text</code>, the <code>lengthAfter</code> characters + * beginning at <code>start</code> have just replaced old text that had + * length <code>lengthBefore</code>. It is an error to attempt to make + * changes to <code>text</code> from this callback. + * + * @param text The text the TextView is displaying + * @param start The offset of the start of the range of the text that was + * modified + * @param lengthBefore The length of the former text that has been replaced + * @param lengthAfter The length of the replacement modified text */ - protected void onTextChanged(CharSequence text, - int start, int before, int after) { + protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { + // intentionally empty } /** @@ -6785,6 +6805,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @param selEnd The new selection end location. */ protected void onSelectionChanged(int selStart, int selEnd) { + // intentionally empty } /** @@ -7131,7 +7152,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // The DecorView does not have focus when the 'Done' ExtractEditText button is - // pressed. Since it is the ViewRoot's mView, it requests focus before + // pressed. Since it is the ViewAncestor's mView, it requests focus before // ExtractEditText clears focus, which gives focus to the ExtractEditText. // This special case ensure that we keep current selection in that case. // It would be better to know why the DecorView does not have focus at that time. @@ -7200,14 +7221,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } super.onFocusChanged(focused, direction, previouslyFocusedRect); - - // Performed after super.onFocusChanged so that this TextView is registered and can ask for - // the IME. Showing the IME while focus is moved using the D-Pad is a bad idea, however this - // does not happen in that case (using the arrows on a bluetooth keyboard). - if (focused && isTextEditable()) { - final InputMethodManager imm = InputMethodManager.peekInstance(); - if (imm != null) imm.showSoftInput(this, 0); - } } private int getLastTapPosition() { @@ -7247,11 +7260,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mInputContentType.enterDown = false; } hideControllers(); + removeAllSuggestionSpans(); } startStopMarquee(hasWindowFocus); } + private void removeAllSuggestionSpans() { + if (mText instanceof Editable) { + Editable editable = ((Editable) mText); + SuggestionSpan[] spans = editable.getSpans(0, mText.length(), SuggestionSpan.class); + final int length = spans.length; + for (int i = 0; i < length; i++) { + editable.removeSpan(spans[i]); + } + } + } + @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); @@ -7326,9 +7351,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener && mText instanceof Spannable && mLayout != null) { boolean handled = false; - final int oldScrollX = mScrollX; - final int oldScrollY = mScrollY; - if (mMovement != null) { handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); } @@ -7345,27 +7367,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - if (isTextEditable() || mTextIsSelectable) { - if (mScrollX != oldScrollX || mScrollY != oldScrollY) { // TODO remove - // Hide insertion anchor while scrolling. Leave selection. - hideInsertionPointCursorController(); // TODO any motion should hide it + if ((isTextEditable() || mTextIsSelectable) && touchIsFinished) { + // Show the IME, except when selecting in read-only text. + if (!mTextIsSelectable) { + final InputMethodManager imm = InputMethodManager.peekInstance(); + handled |= imm != null && imm.showSoftInput(this, 0); } - if (touchIsFinished) { - // Show the IME, except when selecting in read-only text. - if (!mTextIsSelectable) { - final InputMethodManager imm = InputMethodManager.peekInstance(); - handled |= imm != null && imm.showSoftInput(this, 0); - } - - boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect(); - if (!selectAllGotFocus && hasSelection()) { - startSelectionActionMode(); - } else { - stopSelectionActionMode(); - if (hasInsertionController() && !selectAllGotFocus && mText.length() > 0) { - getInsertionController().show(); - } + boolean selectAllGotFocus = mSelectAllOnFocus && didTouchFocusSelect(); + if (!selectAllGotFocus && hasSelection()) { + startSelectionActionMode(); + } else { + stopSelectionActionMode(); + hideSuggestions(); + if (hasInsertionController() && !selectAllGotFocus && mText.length() > 0) { + getInsertionController().show(); } } } @@ -7752,78 +7768,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener type == Character.DECIMAL_DIGIT_NUMBER); } - /** - * Returns the offsets delimiting the 'word' located at position offset. - * - * @param offset An offset in the text. - * @return The offsets for the start and end of the word located at <code>offset</code>. - * The two ints offsets are packed in a long using {@link #packRangeInLong(int, int)}. - * Returns -1 if no valid word was found. - */ - private long getWordLimitsAt(int offset) { - int klass = mInputType & InputType.TYPE_MASK_CLASS; - int variation = mInputType & InputType.TYPE_MASK_VARIATION; - - // Text selection is not permitted in password fields - if (hasPasswordTransformationMethod()) { - return -1; - } - - final int len = mText.length(); - - // Specific text fields: always select the entire text - if (klass == InputType.TYPE_CLASS_NUMBER || - klass == InputType.TYPE_CLASS_PHONE || - klass == InputType.TYPE_CLASS_DATETIME || - variation == InputType.TYPE_TEXT_VARIATION_URI || - variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || - variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS || - variation == InputType.TYPE_TEXT_VARIATION_FILTER) { - return len > 0 ? packRangeInLong(0, len) : -1; - } - - int end = Math.min(offset, len); - if (end < 0) { - return -1; - } - - final int MAX_LENGTH = 48; - int start = end; - - for (; start > 0; start--) { - final char c = mTransformed.charAt(start - 1); - final int type = Character.getType(c); - if (start == end && type == Character.OTHER_PUNCTUATION) { - // Cases where the text ends with a '.' and we select from the end of the line - // (right after the dot), or when we select from the space character in "aaa, bbb". - continue; - } - if (type == Character.SURROGATE) { // Two Character codepoint - end = start - 1; // Recheck as a pair when scanning forward - continue; - } - if (!isWordCharacter(c, type)) break; - if ((end - start) > MAX_LENGTH) return -1; - } - - for (; end < len; end++) { - final int c = Character.codePointAt(mTransformed, end); - final int type = Character.getType(c); - if (!isWordCharacter(c, type)) break; - if ((end - start) > MAX_LENGTH) return -1; - if (c > 0xFFFF) { // Two Character codepoint - end++; - } - } - - if (start == end) { - return -1; - } - - // Two ints packed in a long - return packRangeInLong(start, end); - } - private static long packRangeInLong(int start, int end) { return (((long) start) << 32) | end; } @@ -7836,21 +7780,40 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return (int) (range & 0x00000000FFFFFFFFL); } - private void selectAll() { - Selection.setSelection((Spannable) mText, 0, mText.length()); + private boolean selectAll() { + final int length = mText.length(); + Selection.setSelection((Spannable) mText, 0, length); + return length > 0; } - private void selectCurrentWord() { + /** + * Adjusts selection to the word under last touch offset. + * Return true if the operation was successfully performed. + */ + private boolean selectCurrentWord() { if (!canSelectText()) { - return; + return false; } if (hasPasswordTransformationMethod()) { // Always select all on a password field. // Cut/copy menu entries are not available for passwords, but being able to select all // is however useful to delete or paste to replace the entire content. - selectAll(); - return; + return selectAll(); + } + + int klass = mInputType & InputType.TYPE_MASK_CLASS; + int variation = mInputType & InputType.TYPE_MASK_VARIATION; + + // Specific text field types: select the entire text for these + if (klass == InputType.TYPE_CLASS_NUMBER || + klass == InputType.TYPE_CLASS_PHONE || + klass == InputType.TYPE_CLASS_DATETIME || + variation == InputType.TYPE_TEXT_VARIATION_URI || + variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || + variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS || + variation == InputType.TYPE_TEXT_VARIATION_FILTER) { + return selectAll(); } long lastTouchOffsets = getLastTouchOffsets(); @@ -7866,22 +7829,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener selectionStart = ((Spanned) mText).getSpanStart(url); selectionEnd = ((Spanned) mText).getSpanEnd(url); } else { - long wordLimits = getWordLimitsAt(minOffset); - if (wordLimits >= 0) { - selectionStart = extractRangeStartFromLong(wordLimits); - } else { - selectionStart = Math.max(minOffset - 5, 0); + if (mWordIterator == null) { + mWordIterator = new WordIterator(); } + // WordIerator handles text changes, this is a no-op if text in unchanged. + mWordIterator.setCharSequence(mText); - wordLimits = getWordLimitsAt(maxOffset); - if (wordLimits >= 0) { - selectionEnd = extractRangeEndFromLong(wordLimits); - } else { - selectionEnd = Math.min(maxOffset + 5, mText.length()); - } + selectionStart = mWordIterator.getBeginning(minOffset); + if (selectionStart == BreakIterator.DONE) return false; + + selectionEnd = mWordIterator.getEnd(maxOffset); + if (selectionEnd == BreakIterator.DONE) return false; } Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); + return true; } private long getLastTouchOffsets() { @@ -7900,28 +7862,27 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (!isShown()) { - return false; - } + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); final boolean isPassword = hasPasswordTransformationMethod(); - if (!isPassword) { CharSequence text = getText(); if (TextUtils.isEmpty(text)) { text = getHint(); } if (!TextUtils.isEmpty(text)) { - if (text.length() > AccessibilityEvent.MAX_TEXT_LENGTH) { - text = text.subSequence(0, AccessibilityEvent.MAX_TEXT_LENGTH + 1); - } event.getText().add(text); } - } else { - event.setPassword(isPassword); } - return false; + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + final boolean isPassword = hasPasswordTransformationMethod(); + event.setPassword(isPassword); } void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, @@ -8009,6 +7970,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * this will be {@link android.R.id#copyUrl}, {@link android.R.id#selectTextMode}, * {@link android.R.id#selectAll}, {@link android.R.id#paste}, {@link android.R.id#cut} * or {@link android.R.id#copy}. + * + * @return true if the context menu item action was performed. */ public boolean onTextContextMenuItem(int id) { int min = 0; @@ -8045,7 +8008,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case ID_SELECTION_MODE: if (mSelectionActionMode != null) { // Selection mode is already started, simply change selected part. - updateSelectedRegion(); + selectCurrentWord(); } else { startSelectionActionMode(); } @@ -8053,7 +8016,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case ID_SELECT_ALL: // This does not enter text selection mode. Text is highlighted, so that it can be - // bulk edited, like selectAllOnFocus does. + // bulk edited, like selectAllOnFocus does. Returns true even if text is empty. selectAll(); return true; @@ -8179,8 +8142,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mInsertionControllerEnabled) { final int offset = getOffset(mLastDownPositionX, mLastDownPositionY); stopSelectionActionMode(); - Selection.setSelection((Spannable)mText, offset); - getInsertionController().show(0); + Selection.setSelection((Spannable) mText, offset); + getInsertionController().showWithPaste(); handled = true; } @@ -8195,8 +8158,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0); stopSelectionActionMode(); } else { - // New selection at touch position - updateSelectedRegion(); + selectCurrentWord(); } handled = true; } @@ -8212,17 +8174,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return handled; } - /** - * When selection mode is already started, this method simply updates the selected part of text - * to the text under the finger. - */ - private void updateSelectedRegion() { - // Start a new selection at current position, keep selectionAction mode on - selectCurrentWord(); - // Updates handles' positions - getSelectionController().show(); - } - private boolean touchPositionIsInSelection() { int selectionStart = getSelectionStart(); int selectionEnd = getSelectionEnd(); @@ -8245,6 +8196,424 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); } + private static class SuggestionRangeSpan extends UnderlineSpan { + // TODO themable, would be nice to make it a child class of TextAppearanceSpan, but + // there is no way to have underline and TextAppearanceSpan. + } + + private class SuggestionsPopupWindow implements OnClickListener { + private static final int MAX_NUMBER_SUGGESTIONS = 5; + private static final int NO_SUGGESTIONS = -1; + private final PopupWindow mContainer; + private final ViewGroup[] mSuggestionViews = new ViewGroup[2]; + private final int[] mSuggestionViewLayouts = new int[] { + mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout}; + private WordIterator mWordIterator; + private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0]; + + public SuggestionsPopupWindow() { + mContainer = new PopupWindow(TextView.this.mContext, null, + com.android.internal.R.attr.textSuggestionsWindowStyle); + mContainer.setSplitTouchEnabled(true); + mContainer.setClippingEnabled(false); + mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); + + mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + } + + private class SuggestionInfo { + int suggestionStart, suggestionEnd; // range of suggestion item with replacement text + int spanStart, spanEnd; // range in TextView where text should be inserted + SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents + int suggestionIndex; // the index of the suggestion inside suggestionSpan + } + + private ViewGroup getViewGroup(boolean under) { + final int viewIndex = under ? 0 : 1; + ViewGroup viewGroup = mSuggestionViews[viewIndex]; + + if (viewGroup == null) { + final int layout = mSuggestionViewLayouts[viewIndex]; + LayoutInflater inflater = (LayoutInflater) TextView.this.mContext. + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + if (inflater == null) { + throw new IllegalArgumentException( + "Unable to create TextEdit suggestion window inflater"); + } + + View view = inflater.inflate(layout, null); + + if (! (view instanceof ViewGroup)) { + throw new IllegalArgumentException( + "Inflated TextEdit suggestion window is not a ViewGroup: " + view); + } + + viewGroup = (ViewGroup) view; + + // Inflate the suggestion items once and for all. + for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) { + View childView = inflater.inflate(mTextEditSuggestionItemLayout, viewGroup, + false); + + if (! (childView instanceof TextView)) { + throw new IllegalArgumentException( + "Inflated TextEdit suggestion item is not a TextView: " + childView); + } + + childView.setTag(new SuggestionInfo()); + viewGroup.addView(childView); + childView.setOnClickListener(this); + } + + mSuggestionViews[viewIndex] = viewGroup; + } + + return viewGroup; + } + + public void show() { + if (!(mText instanceof Editable)) return; + + final int pos = TextView.this.getSelectionStart(); + Spannable spannable = (Spannable)TextView.this.mText; + SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class); + final int nbSpans = suggestionSpans.length; + + ViewGroup viewGroup = getViewGroup(true); + mContainer.setContentView(viewGroup); + + int totalNbSuggestions = 0; + int spanUnionStart = mText.length(); + int spanUnionEnd = 0; + + for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { + SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; + final int spanStart = spannable.getSpanStart(suggestionSpan); + final int spanEnd = spannable.getSpanEnd(suggestionSpan); + spanUnionStart = Math.min(spanStart, spanUnionStart); + spanUnionEnd = Math.max(spanEnd, spanUnionEnd); + + String[] suggestions = suggestionSpan.getSuggestions(); + int nbSuggestions = suggestions.length; + for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { + TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions); + textView.setText(suggestions[suggestionIndex]); + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + suggestionInfo.spanStart = spanStart; + suggestionInfo.spanEnd = spanEnd; + suggestionInfo.suggestionSpan = suggestionSpan; + suggestionInfo.suggestionIndex = suggestionIndex; + + totalNbSuggestions++; + if (totalNbSuggestions == MAX_NUMBER_SUGGESTIONS) { + // Also end outer for loop + spanIndex = nbSpans; + break; + } + } + } + + if (totalNbSuggestions == 0) { + // TODO Replace by final text, use a dedicated layout, add a fade out timer... + TextView textView = (TextView) viewGroup.getChildAt(0); + textView.setText("No suggestions available"); + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + suggestionInfo.spanStart = NO_SUGGESTIONS; + totalNbSuggestions++; + } else { + if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan(); + ((Editable) mText).setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + for (int i = 0; i < totalNbSuggestions; i++) { + final TextView textView = (TextView) viewGroup.getChildAt(i); + highlightTextDifferences(textView, spanUnionStart, spanUnionEnd); + } + } + + for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) { + viewGroup.getChildAt(i).setVisibility(i < totalNbSuggestions ? VISIBLE : GONE); + } + + final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + viewGroup.measure(size, size); + + positionAtCursor(); + } + + private long[] getWordLimits(CharSequence text) { + if (mWordIterator == null) mWordIterator = new WordIterator(); // TODO locale + mWordIterator.setCharSequence(text); + + // First pass will simply count the number of words to be able to create an array + // Not too expensive since previous break positions are cached by the BreakIterator + int nbWords = 0; + int position = mWordIterator.following(0); + while (position != BreakIterator.DONE) { + nbWords++; + position = mWordIterator.following(position); + } + + int index = 0; + long[] result = new long[nbWords]; + + position = mWordIterator.following(0); + while (position != BreakIterator.DONE) { + int wordStart = mWordIterator.getBeginning(position); + result[index++] = packRangeInLong(wordStart, position); + position = mWordIterator.following(position); + } + + return result; + } + + private TextAppearanceSpan highlightSpan(int index) { + final int length = mHighlightSpans.length; + if (index < length) { + return mHighlightSpans[index]; + } + + // Assumes indexes are requested in sequence: simply append one more item + TextAppearanceSpan[] newArray = new TextAppearanceSpan[length + 1]; + System.arraycopy(mHighlightSpans, 0, newArray, 0, length); + TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext, + android.R.style.TextAppearance_SuggestionHighlight); + newArray[length] = highlightSpan; + mHighlightSpans = newArray; + return highlightSpan; + } + + private void highlightTextDifferences(TextView textView, int unionStart, int unionEnd) { + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + final int spanStart = suggestionInfo.spanStart; + final int spanEnd = suggestionInfo.spanEnd; + + // Remove all text formating by converting to Strings + final String text = textView.getText().toString(); + final String sourceText = mText.subSequence(spanStart, spanEnd).toString(); + + long[] sourceWordLimits = getWordLimits(sourceText); + long[] wordLimits = getWordLimits(text); + + SpannableStringBuilder ssb = new SpannableStringBuilder(); + // span [spanStart, spanEnd] is included in union [spanUnionStart, int spanUnionEnd] + // The final result is made of 3 parts: the text before, between and after the span + // This is the text before, provided for context + ssb.append(mText.subSequence(unionStart, spanStart).toString()); + + // shift is used to offset spans positions wrt span's beginning + final int shift = spanStart - unionStart; + suggestionInfo.suggestionStart = shift; + suggestionInfo.suggestionEnd = shift + text.length(); + + // This is the actual suggestion text, which will be highlighted by the following code + ssb.append(text); + + String[] words = new String[wordLimits.length]; + for (int i = 0; i < wordLimits.length; i++) { + int wordStart = extractRangeStartFromLong(wordLimits[i]); + int wordEnd = extractRangeEndFromLong(wordLimits[i]); + words[i] = text.substring(wordStart, wordEnd); + } + + // Highlighted word algorithm is based on word matching between source and text + // Matching words are found from left to right. TODO: change for RTL languages + // Characters between matching words are highlighted + int previousCommonWordIndex = -1; + int nbHighlightSpans = 0; + for (int i = 0; i < sourceWordLimits.length; i++) { + int wordStart = extractRangeStartFromLong(sourceWordLimits[i]); + int wordEnd = extractRangeEndFromLong(sourceWordLimits[i]); + String sourceWord = sourceText.substring(wordStart, wordEnd); + + for (int j = previousCommonWordIndex + 1; j < words.length; j++) { + if (sourceWord.equals(words[j])) { + if (j != previousCommonWordIndex + 1) { + int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 : + extractRangeEndFromLong(wordLimits[previousCommonWordIndex]); + int lastDifferentPosition = extractRangeStartFromLong(wordLimits[j]); + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + firstDifferentPosition, shift + lastDifferentPosition, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + // Compare characters between words + int previousSourceWordEnd = i == 0 ? 0 : + extractRangeEndFromLong(sourceWordLimits[i - 1]); + int sourceWordStart = extractRangeStartFromLong(sourceWordLimits[i]); + String sourceSpaces = sourceText.substring(previousSourceWordEnd, + sourceWordStart); + + int previousWordEnd = j == 0 ? 0 : + extractRangeEndFromLong(wordLimits[j - 1]); + int currentWordStart = extractRangeStartFromLong(wordLimits[j]); + String textSpaces = text.substring(previousWordEnd, currentWordStart); + + if (!sourceSpaces.equals(textSpaces)) { + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + previousWordEnd, shift + currentWordStart, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + previousCommonWordIndex = j; + break; + } + } + } + + // Finally, compare ends of Strings + if (previousCommonWordIndex < words.length - 1) { + int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 : + extractRangeEndFromLong(wordLimits[previousCommonWordIndex]); + int lastDifferentPosition = textView.length(); + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + firstDifferentPosition, shift + lastDifferentPosition, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + int lastSourceWordEnd = sourceWordLimits.length == 0 ? 0 : + extractRangeEndFromLong(sourceWordLimits[sourceWordLimits.length - 1]); + String sourceSpaces = sourceText.substring(lastSourceWordEnd, sourceText.length()); + + int lastCommonTextWordEnd = previousCommonWordIndex < 0 ? 0 : + extractRangeEndFromLong(wordLimits[previousCommonWordIndex]); + String textSpaces = text.substring(lastCommonTextWordEnd, textView.length()); + + if (!sourceSpaces.equals(textSpaces) && textSpaces.length() > 0) { + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + lastCommonTextWordEnd, shift + textView.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + // Final part, text after the current suggestion range. + ssb.append(mText.subSequence(spanEnd, unionEnd).toString()); + textView.setText(ssb); + } + + public void hide() { + if ((mText instanceof Editable) && mSuggestionRangeSpan != null) { + ((Editable) mText).removeSpan(mSuggestionRangeSpan); + } + mContainer.dismiss(); + } + + @Override + public void onClick(View view) { + if (view instanceof TextView) { + TextView textView = (TextView) view; + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + final int spanStart = suggestionInfo.spanStart; + final int spanEnd = suggestionInfo.spanEnd; + if (spanStart != NO_SUGGESTIONS) { + // SuggestionSpans are removed by replace: save them before + Editable editable = ((Editable) mText); + SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd, + SuggestionSpan.class); + final int length = suggestionSpans.length; + int[] suggestionSpansStarts = new int[length]; + int[] suggestionSpansEnds = new int[length]; + int[] suggestionSpansFlags = new int[length]; + for (int i = 0; i < length; i++) { + final SuggestionSpan suggestionSpan = suggestionSpans[i]; + suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan); + suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan); + suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan); + } + + final int suggestionStart = suggestionInfo.suggestionStart; + final int suggestionEnd = suggestionInfo.suggestionEnd; + final String suggestion = textView.getText().subSequence( + suggestionStart, suggestionEnd).toString(); + final String originalText = mText.subSequence(spanStart, spanEnd).toString(); + ((Editable) mText).replace(spanStart, spanEnd, suggestion); + + // Swap text content between actual text and Suggestion span + String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions(); + suggestions[suggestionInfo.suggestionIndex] = originalText; + + // Restore previous SuggestionSpans + final int lengthDifference = suggestion.length() - (spanEnd - spanStart); + for (int i = 0; i < length; i++) { + // Only spans that include the modified region make sense after replacement + // Spans partially included in the replaced region are removed, there is no + // way to assign them a valid range after replacement + if (suggestionSpansStarts[i] <= spanStart && + suggestionSpansEnds[i] >= spanEnd) { + editable.setSpan(suggestionSpans[i], suggestionSpansStarts[i], + suggestionSpansEnds[i] + lengthDifference, + suggestionSpansFlags[i]); + } + } + } + } + hide(); + } + + void positionAtCursor() { + View contentView = mContainer.getContentView(); + int width = contentView.getMeasuredWidth(); + int height = contentView.getMeasuredHeight(); + final int offset = TextView.this.getSelectionStart(); + final int line = mLayout.getLineForOffset(offset); + final int lineBottom = mLayout.getLineBottom(line); + float primaryHorizontal = mLayout.getPrimaryHorizontal(offset); + + final Rect bounds = sCursorControllerTempRect; + bounds.left = (int) (primaryHorizontal - width / 2.0f); + bounds.top = lineBottom; + + bounds.right = bounds.left + width; + bounds.bottom = bounds.top + height; + + convertFromViewportToContentCoordinates(bounds); + + final int[] coords = mTempCoords; + TextView.this.getLocationInWindow(coords); + coords[0] += bounds.left; + coords[1] += bounds.top; + + final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); + final int screenHeight = displayMetrics.heightPixels; + + // Vertical clipping + if (coords[1] + height > screenHeight) { + // Try to position above current line instead + // TODO use top layout instead, reverse suggestion order, + // try full screen vertical down if it still does not fit. TBD with designers. + + // Update dimensions from new view + contentView = mContainer.getContentView(); + width = contentView.getMeasuredWidth(); + height = contentView.getMeasuredHeight(); + + final int lineTop = mLayout.getLineTop(line); + final int lineHeight = lineBottom - lineTop; + coords[1] -= height + lineHeight; + } + + // Horizontal clipping + coords[0] = Math.max(0, coords[0]); + coords[0] = Math.min(displayMetrics.widthPixels - width, coords[0]); + + mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]); + } + } + + void showSuggestions() { + if (mSuggestionsPopupWindow == null) { + mSuggestionsPopupWindow = new SuggestionsPopupWindow(); + } + hideControllers(); + mSuggestionsPopupWindow.show(); + } + + void hideSuggestions() { + if (mSuggestionsPopupWindow != null) { + mSuggestionsPopupWindow.hide(); + } + } + /** * If provided, this ActionMode.Callback will be used to create the ActionMode when text * selection is initiated in this View. @@ -8296,9 +8665,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return false; } - if (!hasSelection()) { - // If selection mode is started after a device rotation, there is already a selection. - selectCurrentWord(); + boolean currentWordSelected = selectCurrentWord(); + if (!currentWordSelected) { + // No word found under cursor or text selection not permitted. + return false; } ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); @@ -8329,16 +8699,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = clipboard.getPrimaryClip(); if (clip != null) { - boolean didfirst = false; + boolean didFirst = false; for (int i=0; i<clip.getItemCount(); i++) { CharSequence paste = clip.getItemAt(i).coerceToText(getContext()); if (paste != null) { - if (!didfirst) { + if (!didFirst) { long minMax = prepareSpacesAroundPaste(min, max, paste); min = extractRangeStartFromLong(minMax); max = extractRangeEndFromLong(minMax); Selection.setSelection((Spannable) mText, max); ((Editable) mText).replace(min, max, paste); + didFirst = true; } else { ((Editable) mText).insert(getSelectionEnd(), "\n"); ((Editable) mText).insert(getSelectionEnd(), paste); @@ -8453,65 +8824,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - /** - * A CursorController instance can be used to control a cursor in the text. - * It is not used outside of {@link TextView}. - * @hide - */ - private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { - /** - * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. - * See also {@link #hide()}. - */ - public void show(); - - /** - * Hide the cursor controller from screen. - * See also {@link #show()}. - */ - public void hide(); - - /** - * @return true if the CursorController is currently visible - */ - public boolean isShowing(); - - /** - * Update the controller's position. - */ - public void updatePosition(HandleView handle, int x, int y); - - public void updateOffset(HandleView handle, int offset); - - public void updatePosition(); - - public int getCurrentOffset(HandleView handle); - - /** - * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller - * a chance to become active and/or visible. - * @param event The touch event - */ - public boolean onTouchEvent(MotionEvent event); - - /** - * Called when the view is detached from window. Perform house keeping task, such as - * stopping Runnable thread that would otherwise keep a reference on the context, thus - * preventing the activity to be recycled. - */ - public void onDetached(); - } - - private class PastePopupMenu implements OnClickListener { + private class PastePopupWindow implements OnClickListener { private final PopupWindow mContainer; - private int mPositionX; - private int mPositionY; private final View[] mPasteViews = new View[4]; private final int[] mPasteViewLayouts = new int[] { mTextEditPasteWindowLayout, mTextEditNoPasteWindowLayout, mTextEditSidePasteWindowLayout, mTextEditSideNoPasteWindowLayout }; - public PastePopupMenu() { + public PastePopupWindow() { mContainer = new PopupWindow(TextView.this.mContext, null, com.android.internal.R.attr.textSelectHandleWindowStyle); mContainer.setSplitTouchEnabled(true); @@ -8594,14 +8914,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener convertFromViewportToContentCoordinates(bounds); - mPositionX = bounds.left; - mPositionY = bounds.top; - - final int[] coords = mTempCoords; TextView.this.getLocationInWindow(coords); - coords[0] += mPositionX; - coords[1] += mPositionY; + coords[0] += bounds.left; + coords[1] += bounds.top; final int screenWidth = mContext.getResources().getDisplayMetrics().widthPixels; if (coords[1] < 0) { @@ -8637,32 +8953,43 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private class HandleView extends View implements ViewTreeObserver.OnPreDrawListener { - private Drawable mDrawable; + private abstract class HandleView extends View implements ViewTreeObserver.OnPreDrawListener { + protected Drawable mDrawable; private final PopupWindow mContainer; // Position with respect to the parent TextView private int mPositionX, mPositionY; - private final CursorController mController; private boolean mIsDragging; // Offset from touch position to mPosition private float mTouchToWindowOffsetX, mTouchToWindowOffsetY; - private float mHotspotX; + protected float mHotspotX; // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up private float mTouchOffsetY; // Where the touch position should be on the handle to ensure a maximum cursor visibility private float mIdealVerticalOffset; - // Parent's (TextView) position in window + // Parent's (TextView) previous position in window private int mLastParentX, mLastParentY; - private float mDownPositionX, mDownPositionY; // PopupWindow container absolute position with respect to the enclosing window private int mContainerPositionX, mContainerPositionY; // Visible or not (scrolled off screen), whether or not this handle should be visible private boolean mIsActive = false; - // The insertion handle can have an associated PastePopupMenu - private boolean mIsInsertionHandle = false; - // Used to detect taps on the insertion handle, which will affect the PastePopupMenu - private long mTouchTimer; - private PastePopupMenu mPastePopupWindow; + + public HandleView() { + super(TextView.this.mContext); + mContainer = new PopupWindow(TextView.this.mContext, null, + com.android.internal.R.attr.textSelectHandleWindowStyle); + mContainer.setSplitTouchEnabled(true); + mContainer.setClippingEnabled(false); + mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); + mContainer.setContentView(this); + + initDrawable(); + + final int handleHeight = mDrawable.getIntrinsicHeight(); + mTouchOffsetY = -0.3f * handleHeight; + mIdealVerticalOffset = 0.7f * handleHeight; + } + + protected abstract void initDrawable(); // Touch-up filter: number of previous positions remembered private static final int HISTORY_SIZE = 5; @@ -8703,85 +9030,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (i > 0 && i < iMax && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) { - mController.updateOffset(this, mPreviousOffsets[index]); + updateOffset(mPreviousOffsets[index]); } } - public static final int LEFT = 0; - public static final int CENTER = 1; - public static final int RIGHT = 2; - - public HandleView(CursorController controller, int pos) { - super(TextView.this.mContext); - mController = controller; - mContainer = new PopupWindow(TextView.this.mContext, null, - com.android.internal.R.attr.textSelectHandleWindowStyle); - mContainer.setSplitTouchEnabled(true); - mContainer.setClippingEnabled(false); - mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); - mContainer.setContentView(this); - - setPosition(pos); - } - - private void setPosition(int pos) { - int handleWidth; - switch (pos) { - case LEFT: { - if (mSelectHandleLeft == null) { - mSelectHandleLeft = mContext.getResources().getDrawable( - mTextSelectHandleLeftRes); - } - mDrawable = mSelectHandleLeft; - handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = handleWidth * 3.0f / 4.0f; - break; - } - - case RIGHT: { - if (mSelectHandleRight == null) { - mSelectHandleRight = mContext.getResources().getDrawable( - mTextSelectHandleRightRes); - } - mDrawable = mSelectHandleRight; - handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = handleWidth / 4.0f; - break; - } - - case CENTER: - default: { - if (mSelectHandleCenter == null) { - mSelectHandleCenter = mContext.getResources().getDrawable( - mTextSelectHandleRes); - } - mDrawable = mSelectHandleCenter; - handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = handleWidth / 2.0f; - mIsInsertionHandle = true; - break; - } - } - - final int handleHeight = mDrawable.getIntrinsicHeight(); - mTouchOffsetY = -0.3f * handleHeight; - mIdealVerticalOffset = 0.7f * handleHeight; - - invalidate(); - } - @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); } public void show() { - updateContainerPosition(); if (isShowing()) { mContainer.update(mContainerPositionX, mContainerPositionY, mRight - mLeft, mBottom - mTop); - - hidePastePopupWindow(); } else { mContainer.showAtLocation(TextView.this, 0, mContainerPositionX, mContainerPositionY); @@ -8793,10 +9054,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void dismiss() { + protected void dismiss() { mIsDragging = false; mContainer.dismiss(); - hidePastePopupWindow(); } public void hide() { @@ -8827,24 +9087,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int compoundPaddingLeft = getCompoundPaddingLeft(); final int compoundPaddingRight = getCompoundPaddingRight(); - final TextView hostView = TextView.this; + final TextView textView = TextView.this; - if (mTempRect == null) { - mTempRect = new Rect(); - } + if (mTempRect == null) mTempRect = new Rect(); final Rect clip = mTempRect; clip.left = compoundPaddingLeft; clip.top = extendedPaddingTop; - clip.right = hostView.getWidth() - compoundPaddingRight; - clip.bottom = hostView.getHeight() - extendedPaddingBottom; + clip.right = textView.getWidth() - compoundPaddingRight; + clip.bottom = textView.getHeight() - extendedPaddingBottom; - final ViewParent parent = hostView.getParent(); - if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) { + final ViewParent parent = textView.getParent(); + if (parent == null || !parent.getChildVisibleRect(textView, clip, null)) { return false; } final int[] coords = mTempCoords; - hostView.getLocationInWindow(coords); + textView.getLocationInWindow(coords); final int posX = coords[0] + mPositionX + (int) mHotspotX; final int posY = coords[1] + mPositionY; @@ -8853,45 +9111,52 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener posY >= clip.top && posY <= clip.bottom; } - private void moveTo(int x, int y) { - mPositionX = x - TextView.this.mScrollX; - mPositionY = y - TextView.this.mScrollY; + public abstract int getCurrentCursorOffset(); - if (mIsDragging) { - TextView.this.getLocationInWindow(mTempCoords); - if (mTempCoords[0] != mLastParentX || mTempCoords[1] != mLastParentY) { - mTouchToWindowOffsetX += mTempCoords[0] - mLastParentX; - mTouchToWindowOffsetY += mTempCoords[1] - mLastParentY; - mLastParentX = mTempCoords[0]; - mLastParentY = mTempCoords[1]; - } - // Hide paste popup window as soon as the handle is dragged. - hidePastePopupWindow(); - } + public abstract void updateOffset(int offset); + + public abstract void updatePosition(int x, int y); + + protected void positionAtCursorOffset(int offset) { + addPositionToTouchUpFilter(offset); + final int line = mLayout.getLineForOffset(offset); + final int lineBottom = mLayout.getLineBottom(line); + + mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX); + mPositionY = lineBottom; + + // Take TextView's padding into account. + mPositionX += viewportToContentHorizontalOffset(); + mPositionY += viewportToContentVerticalOffset(); } - /** - * Updates the global container's position. - * @return whether or not the position has actually changed - */ - private boolean updateContainerPosition() { - // TODO Prevent this using different HandleView subclasses - mController.updateOffset(this, mController.getCurrentOffset(this)); + protected boolean updateContainerPosition() { + positionAtCursorOffset(getCurrentCursorOffset()); + + final int previousContainerPositionX = mContainerPositionX; + final int previousContainerPositionY = mContainerPositionY; + TextView.this.getLocationInWindow(mTempCoords); - final int containerPositionX = mTempCoords[0] + mPositionX; - final int containerPositionY = mTempCoords[1] + mPositionY; + mContainerPositionX = mTempCoords[0] + mPositionX; + mContainerPositionY = mTempCoords[1] + mPositionY; - if (containerPositionX != mContainerPositionX || - containerPositionY != mContainerPositionY) { - mContainerPositionX = containerPositionX; - mContainerPositionY = containerPositionY; - return true; - } - return false; + return (previousContainerPositionX != mContainerPositionX || + previousContainerPositionY != mContainerPositionY); } public boolean onPreDraw() { if (updateContainerPosition()) { + if (mIsDragging) { + if (mTempCoords[0] != mLastParentX || mTempCoords[1] != mLastParentY) { + mTouchToWindowOffsetX += mTempCoords[0] - mLastParentX; + mTouchToWindowOffsetY += mTempCoords[1] - mLastParentY; + mLastParentX = mTempCoords[0]; + mLastParentY = mTempCoords[1]; + } + } + + onHandleMoved(); + if (isPositionVisible()) { mContainer.update(mContainerPositionX, mContainerPositionY, mRight - mLeft, mBottom - mTop); @@ -8904,9 +9169,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener dismiss(); } } - - // Hide paste popup as soon as the view is scrolled or moved - hidePastePopupWindow(); } return true; } @@ -8921,20 +9183,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean onTouchEvent(MotionEvent ev) { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { - startTouchUpFilter(mController.getCurrentOffset(this)); - mDownPositionX = ev.getRawX(); - mDownPositionY = ev.getRawY(); - mTouchToWindowOffsetX = mDownPositionX - mPositionX; - mTouchToWindowOffsetY = mDownPositionY - mPositionY; + startTouchUpFilter(getCurrentCursorOffset()); + mTouchToWindowOffsetX = ev.getRawX() - mPositionX; + mTouchToWindowOffsetY = ev.getRawY() - mPositionY; final int[] coords = mTempCoords; TextView.this.getLocationInWindow(coords); mLastParentX = coords[0]; mLastParentY = coords[1]; mIsDragging = true; - if (mIsInsertionHandle) { - mTouchTimer = SystemClock.uptimeMillis(); - } break; } @@ -8958,27 +9215,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY; - mController.updatePosition(this, Math.round(newPosX), Math.round(newPosY)); + updatePosition(Math.round(newPosX), Math.round(newPosY)); break; } case MotionEvent.ACTION_UP: - if (mIsInsertionHandle) { - long delay = SystemClock.uptimeMillis() - mTouchTimer; - if (delay < ViewConfiguration.getTapTimeout()) { - final float deltaX = mDownPositionX - ev.getRawX(); - final float deltaY = mDownPositionY - ev.getRawY(); - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - if (distanceSquared < mSquaredTouchSlopDistance) { - if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) { - // Tapping on the handle dismisses the displayed paste view, - mPastePopupWindow.hide(); - } else { - ((InsertionPointCursorController) mController).show(0); - } - } - } - } filterOnTouchUp(); mIsDragging = false; break; @@ -8994,60 +9235,36 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return mIsDragging; } - void positionAtCursor(int offset) { - addPositionToTouchUpFilter(offset); - final int width = mDrawable.getIntrinsicWidth(); - final int height = mDrawable.getIntrinsicHeight(); - final int line = mLayout.getLineForOffset(offset); - final int lineBottom = mLayout.getLineBottom(line); - - final Rect bounds = sCursorControllerTempRect; - bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX) + - TextView.this.mScrollX; - bounds.top = lineBottom + TextView.this.mScrollY; - - bounds.right = bounds.left + width; - bounds.bottom = bounds.top + height; - - convertFromViewportToContentCoordinates(bounds); - moveTo(bounds.left, bounds.top); - } - - void showPastePopupWindow() { - if (mIsInsertionHandle) { - if (mPastePopupWindow == null) { - // Lazy initialisation: create when actually shown only. - mPastePopupWindow = new PastePopupMenu(); - } - mPastePopupWindow.show(); - } + void onHandleMoved() { + // Does nothing by default } - void hidePastePopupWindow() { - if (mPastePopupWindow != null) { - mPastePopupWindow.hide(); - } + public void onDetached() { + // Should be overriden to clean possible Runnable } } - private class InsertionPointCursorController implements CursorController { + private class InsertionHandleView extends HandleView { private static final int DELAY_BEFORE_FADE_OUT = 4000; - private static final int DELAY_BEFORE_PASTE = 2000; - private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; + private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds - // The cursor controller image. Lazily created. - private HandleView mHandle; + // Used to detect taps on the insertion handle, which will affect the PastePopupWindow + private long mTouchTimer; + private float mDownPositionX, mDownPositionY; + private PastePopupWindow mPastePopupWindow; private Runnable mHider; private Runnable mPastePopupShower; + @Override public void show() { - show(DELAY_BEFORE_PASTE); + super.show(); + hideDelayed(); + hidePastePopupWindow(); } public void show(int delayBeforePaste) { - getHandle().show(); - hideDelayed(); - removePastePopupCallback(); + show(); + final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime; if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) { delayBeforePaste = 0; @@ -9056,81 +9273,256 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mPastePopupShower == null) { mPastePopupShower = new Runnable() { public void run() { - getHandle().showPastePopupWindow(); + showPastePopupWindow(); } }; } - postDelayed(mPastePopupShower, delayBeforePaste); + TextView.this.postDelayed(mPastePopupShower, delayBeforePaste); } } - private void removePastePopupCallback() { - if (mPastePopupShower != null) { - removeCallbacks(mPastePopupShower); + @Override + protected void dismiss() { + super.dismiss(); + onDetached(); + } + + private void hideDelayed() { + removeHiderCallback(); + if (mHider == null) { + mHider = new Runnable() { + public void run() { + hide(); + } + }; } + TextView.this.postDelayed(mHider, DELAY_BEFORE_FADE_OUT); } private void removeHiderCallback() { if (mHider != null) { - removeCallbacks(mHider); + TextView.this.removeCallbacks(mHider); } } - public void hide() { - if (mHandle != null) { - mHandle.hide(); + @Override + protected void initDrawable() { + if (mSelectHandleCenter == null) { + mSelectHandleCenter = mContext.getResources().getDrawable( + mTextSelectHandleRes); } - removeHiderCallback(); - removePastePopupCallback(); + mDrawable = mSelectHandleCenter; + mHotspotX = mDrawable.getIntrinsicWidth() / 2.0f; } - private void hideDelayed() { - removeHiderCallback(); - if (mHider == null) { - mHider = new Runnable() { - public void run() { - hide(); + @Override + public boolean onTouchEvent(MotionEvent ev) { + final boolean result = super.onTouchEvent(ev); + + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mDownPositionX = ev.getRawX(); + mDownPositionY = ev.getRawY(); + mTouchTimer = SystemClock.uptimeMillis(); + break; + + case MotionEvent.ACTION_UP: + long delay = SystemClock.uptimeMillis() - mTouchTimer; + if (delay < ViewConfiguration.getTapTimeout()) { + final float deltaX = mDownPositionX - ev.getRawX(); + final float deltaY = mDownPositionY - ev.getRawY(); + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; + if (distanceSquared < mSquaredTouchSlopDistance) { + if (mPastePopupWindow != null && mPastePopupWindow.isShowing()) { + // Tapping on the handle dismisses the displayed paste view, + mPastePopupWindow.hide(); + } else { + show(0); + } + } } - }; + hideDelayed(); + break; + + case MotionEvent.ACTION_CANCEL: + hideDelayed(); + break; + + default: + break; } - postDelayed(mHider, DELAY_BEFORE_FADE_OUT); + + return result; } - public boolean isShowing() { - return mHandle != null && mHandle.isShowing(); + @Override + public int getCurrentCursorOffset() { + return TextView.this.getSelectionStart(); + } + + @Override + public void updateOffset(int offset) { + Selection.setSelection((Spannable) mText, offset); } - public void updatePosition(HandleView handle, int x, int y) { - final int previousOffset = getSelectionStart(); - final int newOffset = getOffset(x, y); + @Override + public void updatePosition(int x, int y) { + updateOffset(getOffset(x, y)); + } - if (newOffset != previousOffset) { - updateOffset(handle, newOffset); - removePastePopupCallback(); + void showPastePopupWindow() { + if (mPastePopupWindow == null) { + mPastePopupWindow = new PastePopupWindow(); } - hideDelayed(); + mPastePopupWindow.show(); } - public void updateOffset(HandleView handle, int offset) { - Selection.setSelection((Spannable) mText, offset); - updatePosition(); + @Override + void onHandleMoved() { + removeHiderCallback(); + hidePastePopupWindow(); } - public void updatePosition() { - final int offset = getSelectionStart(); + void hidePastePopupWindow() { + if (mPastePopupShower != null) { + TextView.this.removeCallbacks(mPastePopupShower); + } + if (mPastePopupWindow != null) { + mPastePopupWindow.hide(); + } + } - if (offset < 0) { - // Should never happen, safety check. - Log.w(LOG_TAG, "Update cursor controller position called with no cursor"); - hide(); - return; + @Override + public void onDetached() { + removeHiderCallback(); + hidePastePopupWindow(); + } + } + + private class SelectionStartHandleView extends HandleView { + @Override + protected void initDrawable() { + if (mSelectHandleLeft == null) { + mSelectHandleLeft = mContext.getResources().getDrawable( + mTextSelectHandleLeftRes); } + mDrawable = mSelectHandleLeft; + mHotspotX = mDrawable.getIntrinsicWidth() * 3.0f / 4.0f; + } + + @Override + public int getCurrentCursorOffset() { + return TextView.this.getSelectionStart(); + } + + @Override + public void updateOffset(int offset) { + Selection.setSelection((Spannable) mText, offset, getSelectionEnd()); + } + + @Override + public void updatePosition(int x, int y) { + final int selectionStart = getSelectionStart(); + final int selectionEnd = getSelectionEnd(); + + int offset = getOffset(x, y); + + // No need to redraw when the offset is unchanged + if (offset == selectionStart) return; + // Handles can not cross and selection is at least one character + if (offset >= selectionEnd) offset = selectionEnd - 1; + + Selection.setSelection((Spannable) mText, offset, selectionEnd); + } + } + + private class SelectionEndHandleView extends HandleView { + @Override + protected void initDrawable() { + if (mSelectHandleRight == null) { + mSelectHandleRight = mContext.getResources().getDrawable( + mTextSelectHandleRightRes); + } + mDrawable = mSelectHandleRight; + mHotspotX = mDrawable.getIntrinsicWidth() / 4.0f; + } + + @Override + public int getCurrentCursorOffset() { + return TextView.this.getSelectionEnd(); + } + + @Override + public void updateOffset(int offset) { + Selection.setSelection((Spannable) mText, getSelectionStart(), offset); + } + + @Override + public void updatePosition(int x, int y) { + final int selectionStart = getSelectionStart(); + final int selectionEnd = getSelectionEnd(); + + int offset = getOffset(x, y); + + // No need to redraw when the offset is unchanged + if (offset == selectionEnd) return; + // Handles can not cross and selection is at least one character + if (offset <= selectionStart) offset = selectionStart + 1; + + Selection.setSelection((Spannable) mText, selectionStart, offset); + } + } + + /** + * A CursorController instance can be used to control a cursor in the text. + * It is not used outside of {@link TextView}. + * @hide + */ + private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { + /** + * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. + * See also {@link #hide()}. + */ + public void show(); - getHandle().positionAtCursor(offset); + /** + * Hide the cursor controller from screen. + * See also {@link #show()}. + */ + public void hide(); + + /** + * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller + * a chance to become active and/or visible. + * @param event The touch event + */ + public boolean onTouchEvent(MotionEvent event); + + /** + * Called when the view is detached from window. Perform house keeping task, such as + * stopping Runnable thread that would otherwise keep a reference on the context, thus + * preventing the activity from being recycled. + */ + public void onDetached(); + } + + private class InsertionPointCursorController implements CursorController { + private static final int DELAY_BEFORE_PASTE = 2000; + + private InsertionHandleView mHandle; + + public void show() { + ((InsertionHandleView) getHandle()).show(DELAY_BEFORE_PASTE); } - public int getCurrentOffset(HandleView handle) { - return getSelectionStart(); + public void showWithPaste() { + ((InsertionHandleView) getHandle()).show(0); + } + + public void hide() { + if (mHandle != null) { + mHandle.hide(); + } } public boolean onTouchEvent(MotionEvent ev) { @@ -9145,30 +9537,30 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private HandleView getHandle() { if (mHandle == null) { - mHandle = new HandleView(this, HandleView.CENTER); + mHandle = new InsertionHandleView(); } return mHandle; } @Override public void onDetached() { - removeHiderCallback(); - removePastePopupCallback(); + final ViewTreeObserver observer = getViewTreeObserver(); + observer.removeOnTouchModeChangeListener(this); + + if (mHandle != null) mHandle.onDetached(); } } private class SelectionModifierCursorController implements CursorController { - // The cursor controller images, lazily created when shown. - private HandleView mStartHandle, mEndHandle; + // The cursor controller handles, lazily created when shown. + private SelectionStartHandleView mStartHandle; + private SelectionEndHandleView mEndHandle; // The offsets of that last touch down event. Remembered to start selection there. private int mMinTouchOffset, mMaxTouchOffset; - // Whether selection anchors are active - private boolean mIsShowing; // Double tap detection private long mPreviousTapUpTime = 0; - private int mPreviousTapPositionX; - private int mPreviousTapPositionY; + private int mPreviousTapPositionX, mPreviousTapPositionY; SelectionModifierCursorController() { resetTouchOffsets(); @@ -9180,96 +9572,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // Lazy object creation has to be done before updatePosition() is called. - if (mStartHandle == null) mStartHandle = new HandleView(this, HandleView.LEFT); - if (mEndHandle == null) mEndHandle = new HandleView(this, HandleView.RIGHT); - - mIsShowing = true; + if (mStartHandle == null) mStartHandle = new SelectionStartHandleView(); + if (mEndHandle == null) mEndHandle = new SelectionEndHandleView(); mStartHandle.show(); mEndHandle.show(); hideInsertionPointCursorController(); + hideSuggestions(); } public void hide() { if (mStartHandle != null) mStartHandle.hide(); if (mEndHandle != null) mEndHandle.hide(); - mIsShowing = false; - } - - public boolean isShowing() { - return mIsShowing; - } - - public void updatePosition(HandleView handle, int x, int y) { - int selectionStart = getSelectionStart(); - int selectionEnd = getSelectionEnd(); - - int offset = getOffset(x, y); - - // Handle the case where start and end are swapped, making sure start <= end - if (handle == mStartHandle) { - if (selectionStart == offset || offset > selectionEnd) { - return; // no change, no need to redraw; - } - // If the user "closes" the selection entirely they were probably trying to - // select a single character. Help them out. - if (offset == selectionEnd) { - offset = selectionEnd - 1; - } - selectionStart = offset; - } else { - if (selectionEnd == offset || offset < selectionStart) { - return; // no change, no need to redraw; - } - // If the user "closes" the selection entirely they were probably trying to - // select a single character. Help them out. - if (offset == selectionStart) { - offset = selectionStart + 1; - } - selectionEnd = offset; - } - - Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); - updatePosition(); - } - - public void updateOffset(HandleView handle, int offset) { - int start = getSelectionStart(); - int end = getSelectionEnd(); - - if (mStartHandle == handle) { - start = offset; - } else { - end = offset; - } - - Selection.setSelection((Spannable) mText, start, end); - updatePosition(); - } - - public void updatePosition() { - if (!isShowing()) { - return; - } - - final int selectionStart = getSelectionStart(); - final int selectionEnd = getSelectionEnd(); - - if ((selectionStart < 0) || (selectionEnd < 0)) { - // Should never happen, safety check. - Log.w(LOG_TAG, "Update selection controller position called with no cursor"); - hide(); - return; - } - - // The handles have been created since the controller isShowing(). - mStartHandle.positionAtCursor(selectionStart); - mEndHandle.positionAtCursor(selectionEnd); - } - - public int getCurrentOffset(HandleView handle) { - return mStartHandle == handle ? getSelectionStart() : getSelectionEnd(); } public boolean onTouchEvent(MotionEvent event) { @@ -9292,7 +9607,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int deltaY = y - mPreviousTapPositionY; final int distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared < mSquaredTouchSlopDistance) { - startSelectionActionMode(); + showSuggestions(); mDiscardNextActionUp = true; } } @@ -9360,7 +9675,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override - public void onDetached() {} + public void onDetached() { + final ViewTreeObserver observer = getViewTreeObserver(); + observer.removeOnTouchModeChangeListener(this); + + if (mStartHandle != null) mStartHandle.onDetached(); + if (mEndHandle != null) mEndHandle.onDetached(); + } } private void hideInsertionPointCursorController() { @@ -9376,6 +9697,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private void hideControllers() { hideInsertionPointCursorController(); stopSelectionActionMode(); + hideSuggestions(); } /** diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index 029d690..423e735 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -409,7 +409,9 @@ public class TimePicker extends FrameLayout { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + int flags = DateUtils.FORMAT_SHOW_TIME; if (mIs24HourView) { flags |= DateUtils.FORMAT_24HOUR; @@ -421,7 +423,6 @@ public class TimePicker extends FrameLayout { String selectedDateUtterance = DateUtils.formatDateTime(mContext, mTempCalendar.getTimeInMillis(), flags); event.getText().add(selectedDateUtterance); - return true; } private void updateHourControl() { diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index 450c966..9e37c7b 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -33,7 +33,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; -import android.view.ViewRoot; +import android.view.ViewAncestor; import android.view.WindowManager; import android.view.View.OnClickListener; import android.view.WindowManager.LayoutParams; @@ -501,7 +501,7 @@ public class ZoomButtonsController implements View.OnTouchListener { } else { - ViewRoot viewRoot = getOwnerViewRoot(); + ViewAncestor viewRoot = getOwnerViewAncestor(); if (viewRoot != null) { viewRoot.dispatchKey(event); } @@ -526,15 +526,15 @@ public class ZoomButtonsController implements View.OnTouchListener { } } - private ViewRoot getOwnerViewRoot() { + private ViewAncestor getOwnerViewAncestor() { View rootViewOfOwner = mOwnerView.getRootView(); if (rootViewOfOwner == null) { return null; } ViewParent parentOfRootView = rootViewOfOwner.getParent(); - if (parentOfRootView instanceof ViewRoot) { - return (ViewRoot) parentOfRootView; + if (parentOfRootView instanceof ViewAncestor) { + return (ViewAncestor) parentOfRootView; } else { return null; } diff --git a/core/java/com/android/internal/app/ActionBarImpl.java b/core/java/com/android/internal/app/ActionBarImpl.java index 8f1354b..1e576ce 100644 --- a/core/java/com/android/internal/app/ActionBarImpl.java +++ b/core/java/com/android/internal/app/ActionBarImpl.java @@ -19,19 +19,20 @@ package com.android.internal.app; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuPopupHelper; import com.android.internal.view.menu.SubMenuBuilder; +import com.android.internal.widget.AbsActionBarView; import com.android.internal.widget.ActionBarContainer; import com.android.internal.widget.ActionBarContextView; import com.android.internal.widget.ActionBarView; import android.animation.Animator; import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.app.ActionBar; import android.app.Activity; import android.app.Dialog; -import android.app.Fragment; import android.app.FragmentTransaction; import android.content.Context; import android.graphics.drawable.Drawable; @@ -42,10 +43,10 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.view.Window; import android.view.animation.DecelerateInterpolator; -import android.widget.FrameLayout; -import android.widget.LinearLayout; +import android.widget.HorizontalScrollView; import android.widget.SpinnerAdapter; import java.lang.ref.WeakReference; @@ -59,8 +60,7 @@ import java.util.ArrayList; * which is normally hidden. */ public class ActionBarImpl extends ActionBar { - private static final int NORMAL_VIEW = 0; - private static final int CONTEXT_VIEW = 1; + private static final String TAG = "ActionBarImpl"; private Context mContext; private Activity mActivity; @@ -68,9 +68,10 @@ public class ActionBarImpl extends ActionBar { private ActionBarContainer mContainerView; private ActionBarView mActionView; - private ActionBarContextView mUpperContextView; - private LinearLayout mLowerContextView; + private ActionBarContextView mContextView; + private ActionBarContainer mSplitView; private View mContentView; + private ViewGroup mExternalTabView; private ArrayList<TabImpl> mTabs = new ArrayList<TabImpl>(); @@ -92,60 +93,14 @@ public class ActionBarImpl extends ActionBar { final Handler mHandler = new Handler(); - private Animator mCurrentAnim; + private Animator mCurrentShowAnim; + private Animator mCurrentModeAnim; private boolean mShowHideAnimationEnabled; + boolean mWasHiddenBeforeMode; private static final TimeInterpolator sFadeOutInterpolator = new DecelerateInterpolator(); - final AnimatorListener[] mAfterAnimation = new AnimatorListener[] { - new AnimatorListener() { // NORMAL_VIEW - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - if (mLowerContextView != null) { - mLowerContextView.removeAllViews(); - } - mCurrentAnim = null; - hideAllExcept(NORMAL_VIEW); - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - }, - new AnimatorListener() { // CONTEXT_VIEW - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - mCurrentAnim = null; - hideAllExcept(CONTEXT_VIEW); - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - } - }; - - final AnimatorListener mHideListener = new AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - + final AnimatorListener mHideListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mContentView != null) { @@ -153,36 +108,16 @@ public class ActionBarImpl extends ActionBar { } mContainerView.setVisibility(View.GONE); mContainerView.setTransitioning(false); - mCurrentAnim = null; - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { + mCurrentShowAnim = null; } }; - final AnimatorListener mShowListener = new AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - + final AnimatorListener mShowListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mCurrentAnim = null; + mCurrentShowAnim = null; mContainerView.requestLayout(); } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } }; public ActionBarImpl(Activity activity) { @@ -203,21 +138,33 @@ public class ActionBarImpl extends ActionBar { private void init(View decor) { mContext = decor.getContext(); mActionView = (ActionBarView) decor.findViewById(com.android.internal.R.id.action_bar); - mUpperContextView = (ActionBarContextView) decor.findViewById( + mContextView = (ActionBarContextView) decor.findViewById( com.android.internal.R.id.action_context_bar); - mLowerContextView = (LinearLayout) decor.findViewById( - com.android.internal.R.id.lower_action_context_bar); mContainerView = (ActionBarContainer) decor.findViewById( com.android.internal.R.id.action_bar_container); + mSplitView = (ActionBarContainer) decor.findViewById( + com.android.internal.R.id.split_action_bar); - if (mActionView == null || mUpperContextView == null || mContainerView == null) { + if (mActionView == null || mContextView == null || mContainerView == null) { throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + "with a compatible window decor layout"); } - mActionView.setContextView(mUpperContextView); - mContextDisplayMode = mLowerContextView == null ? - CONTEXT_DISPLAY_NORMAL : CONTEXT_DISPLAY_SPLIT; + mActionView.setContextView(mContextView); + mContextDisplayMode = mActionView.isSplitActionBar() ? + CONTEXT_DISPLAY_SPLIT : CONTEXT_DISPLAY_NORMAL; + + if (!mActionView.hasEmbeddedTabs()) { + HorizontalScrollView tabScroller = new HorizontalScrollView(mContext); + ViewGroup tabContainer = mActionView.createTabContainer(); + tabScroller.setHorizontalFadingEdgeEnabled(true); + tabScroller.addView(tabContainer); + tabScroller.setVisibility(getNavigationMode() == NAVIGATION_MODE_TABS ? + View.VISIBLE : View.GONE); + mActionView.setExternalTabLayout(tabContainer); + mContainerView.setTabContainer(tabScroller); + mExternalTabView = tabScroller; + } } /** @@ -229,8 +176,8 @@ public class ActionBarImpl extends ActionBar { */ public void setShowHideAnimationEnabled(boolean enabled) { mShowHideAnimationEnabled = enabled; - if (!enabled && mCurrentAnim != null) { - mCurrentAnim.end(); + if (!enabled && mCurrentShowAnim != null) { + mCurrentShowAnim.end(); } } @@ -285,6 +232,11 @@ public class ActionBarImpl extends ActionBar { } @Override + public void setDisplayDisableHomeEnabled(boolean disableHome) { + setDisplayOptions(disableHome ? DISPLAY_DISABLE_HOME : 0, DISPLAY_DISABLE_HOME); + } + + @Override public void setTitle(int resId) { setTitle(mContext.getString(resId)); } @@ -367,18 +319,18 @@ public class ActionBarImpl extends ActionBar { mActionMode.finish(); } - mUpperContextView.killMode(); + mContextView.killMode(); ActionMode mode = new ActionModeImpl(callback); if (callback.onCreateActionMode(mode, mode.getMenu())) { + mWasHiddenBeforeMode = !isShowing(); mode.invalidate(); - mUpperContextView.initForMode(mode); - animateTo(CONTEXT_VIEW); - if (mLowerContextView != null) { + mContextView.initForMode(mode); + animateToMode(true); + if (mSplitView != null) { // TODO animate this - mLowerContextView.setVisibility(View.VISIBLE); + mSplitView.setVisibility(View.VISIBLE); } mActionMode = mode; - show(); return mode; } return null; @@ -444,7 +396,10 @@ public class ActionBarImpl extends ActionBar { int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : mSavedTabPosition; mActionView.removeTabAt(position); - mTabs.remove(position); + TabImpl removedTab = mTabs.remove(position); + if (removedTab != null) { + removedTab.setPosition(-1); + } final int newTabCount = mTabs.size(); for (int i = position; i < newTabCount; i++) { @@ -498,10 +453,15 @@ public class ActionBarImpl extends ActionBar { @Override public void show() { - if (mCurrentAnim != null) { - mCurrentAnim.end(); + show(true); + } + + void show(boolean markHiddenBeforeMode) { + if (mCurrentShowAnim != null) { + mCurrentShowAnim.end(); } if (mContainerView.getVisibility() == View.VISIBLE) { + if (markHiddenBeforeMode) mWasHiddenBeforeMode = false; return; } mContainerView.setVisibility(View.VISIBLE); @@ -516,18 +476,24 @@ public class ActionBarImpl extends ActionBar { mContainerView.setTranslationY(-mContainerView.getHeight()); b.with(ObjectAnimator.ofFloat(mContainerView, "translationY", 0)); } + if (mSplitView != null) { + mSplitView.setAlpha(0); + b.with(ObjectAnimator.ofFloat(mSplitView, "alpha", 1)); + } anim.addListener(mShowListener); - mCurrentAnim = anim; + mCurrentShowAnim = anim; anim.start(); } else { + mContainerView.setAlpha(1); + mContainerView.setTranslationY(0); mShowListener.onAnimationEnd(null); } } @Override public void hide() { - if (mCurrentAnim != null) { - mCurrentAnim.end(); + if (mCurrentShowAnim != null) { + mCurrentShowAnim.end(); } if (mContainerView.getVisibility() == View.GONE) { return; @@ -544,8 +510,12 @@ public class ActionBarImpl extends ActionBar { b.with(ObjectAnimator.ofFloat(mContainerView, "translationY", -mContainerView.getHeight())); } + if (mSplitView != null) { + mSplitView.setAlpha(1); + b.with(ObjectAnimator.ofFloat(mSplitView, "alpha", 0)); + } anim.addListener(mHideListener); - mCurrentAnim = anim; + mCurrentShowAnim = anim; anim.start(); } else { mHideListener.onAnimationEnd(null); @@ -556,41 +526,14 @@ public class ActionBarImpl extends ActionBar { return mContainerView.getVisibility() == View.VISIBLE; } - private long animateTo(int viewIndex) { - show(); - - AnimatorSet set = new AnimatorSet(); - - final View targetChild = mContainerView.getChildAt(viewIndex); - targetChild.setVisibility(View.VISIBLE); - AnimatorSet.Builder b = set.play(ObjectAnimator.ofFloat(targetChild, "alpha", 1)); - - final int count = mContainerView.getChildCount(); - for (int i = 0; i < count; i++) { - final View child = mContainerView.getChildAt(i); - if (i == viewIndex) { - continue; - } - - if (child.getVisibility() != View.GONE) { - Animator a = ObjectAnimator.ofFloat(child, "alpha", 0); - a.setInterpolator(sFadeOutInterpolator); - b.with(a); - } + void animateToMode(boolean toActionMode) { + show(false); + if (mCurrentModeAnim != null) { + mCurrentModeAnim.end(); } - set.addListener(mAfterAnimation[viewIndex]); - - mCurrentAnim = set; - set.start(); - return set.getDuration(); - } - - private void hideAllExcept(int viewIndex) { - final int count = mContainerView.getChildCount(); - for (int i = 0; i < count; i++) { - mContainerView.getChildAt(i).setVisibility(i == viewIndex ? View.VISIBLE : View.GONE); - } + mActionView.animateToVisibility(toActionMode ? View.GONE : View.VISIBLE); + mContextView.animateToVisibility(toActionMode ? View.VISIBLE : View.GONE); } /** @@ -627,15 +570,15 @@ public class ActionBarImpl extends ActionBar { mCallback.onDestroyActionMode(this); mCallback = null; - animateTo(NORMAL_VIEW); + animateToMode(false); // Clear out the context mode views after the animation finishes - mUpperContextView.closeMode(); - if (mLowerContextView != null && mLowerContextView.getVisibility() != View.GONE) { - // TODO Animate this - mLowerContextView.setVisibility(View.GONE); - } + mContextView.closeMode(); mActionMode = null; + + if (mWasHiddenBeforeMode) { + hide(); + } } @Override @@ -647,18 +590,18 @@ public class ActionBarImpl extends ActionBar { @Override public void setCustomView(View view) { - mUpperContextView.setCustomView(view); + mContextView.setCustomView(view); mCustomView = new WeakReference<View>(view); } @Override public void setSubtitle(CharSequence subtitle) { - mUpperContextView.setSubtitle(subtitle); + mContextView.setSubtitle(subtitle); } @Override public void setTitle(CharSequence title) { - mUpperContextView.setTitle(title); + mContextView.setTitle(title); } @Override @@ -673,12 +616,12 @@ public class ActionBarImpl extends ActionBar { @Override public CharSequence getTitle() { - return mUpperContextView.getTitle(); + return mContextView.getTitle(); } @Override public CharSequence getSubtitle() { - return mUpperContextView.getSubtitle(); + return mContextView.getSubtitle(); } @Override @@ -718,7 +661,7 @@ public class ActionBarImpl extends ActionBar { return; } invalidate(); - mUpperContextView.openOverflowMenu(); + mContextView.showOverflowMenu(); } } @@ -730,7 +673,7 @@ public class ActionBarImpl extends ActionBar { private Object mTag; private Drawable mIcon; private CharSequence mText; - private int mPosition; + private int mPosition = -1; private View mCustomView; @Override @@ -762,6 +705,7 @@ public class ActionBarImpl extends ActionBar { @Override public Tab setCustomView(View view) { mCustomView = view; + if (mPosition >= 0) mActionView.updateTab(mPosition); return this; } @@ -792,6 +736,7 @@ public class ActionBarImpl extends ActionBar { @Override public Tab setIcon(Drawable icon) { mIcon = icon; + if (mPosition >= 0) mActionView.updateTab(mPosition); return this; } @@ -803,6 +748,7 @@ public class ActionBarImpl extends ActionBar { @Override public Tab setText(CharSequence text) { mText = text; + if (mPosition >= 0) mActionView.updateTab(mPosition); return this; } @@ -871,11 +817,17 @@ public class ActionBarImpl extends ActionBar { case NAVIGATION_MODE_TABS: mSavedTabPosition = getSelectedNavigationIndex(); selectTab(null); + if (!mActionView.hasEmbeddedTabs()) { + mExternalTabView.setVisibility(View.GONE); + } break; } mActionView.setNavigationMode(mode); switch (mode) { case NAVIGATION_MODE_TABS: + if (!mActionView.hasEmbeddedTabs()) { + mExternalTabView.setVisibility(View.VISIBLE); + } if (mSavedTabPosition != INVALID_POSITION) { setSelectedNavigationItem(mSavedTabPosition); mSavedTabPosition = INVALID_POSITION; @@ -889,23 +841,24 @@ public class ActionBarImpl extends ActionBar { return mTabs.get(index); } - /** - * This fragment is added when we're keeping a back stack in a tab switch - * transaction. We use it to change the selected tab in the action bar view - * when we back out. - */ - private class SwitchSelectedTabViewFragment extends Fragment { - private int mSelectedTabIndex; - public SwitchSelectedTabViewFragment(int oldSelectedTab) { - mSelectedTabIndex = oldSelectedTab; - } + @Override + public void setIcon(int resId) { + mActionView.setIcon(resId); + } - @Override - public void onDetach() { - if (mSelectedTabIndex >= 0 && mSelectedTabIndex < getTabCount()) { - mActionView.setTabSelected(mSelectedTabIndex); - } - } + @Override + public void setIcon(Drawable icon) { + mActionView.setIcon(icon); + } + + @Override + public void setLogo(int resId) { + mActionView.setLogo(resId); + } + + @Override + public void setLogo(Drawable logo) { + mActionView.setLogo(logo); } } diff --git a/core/java/com/android/internal/app/IMediaContainerService.aidl b/core/java/com/android/internal/app/IMediaContainerService.aidl index aee1626..dd22e25 100755 --- a/core/java/com/android/internal/app/IMediaContainerService.aidl +++ b/core/java/com/android/internal/app/IMediaContainerService.aidl @@ -27,8 +27,9 @@ interface IMediaContainerService { String key, String resFileName); boolean copyResource(in Uri packageURI, in ParcelFileDescriptor outStream); - PackageInfoLite getMinimalPackageInfo(in Uri fileUri, int flags); - boolean checkFreeStorage(boolean external, in Uri fileUri); + PackageInfoLite getMinimalPackageInfo(in Uri fileUri, in int flags, in long threshold); + boolean checkInternalFreeStorage(in Uri fileUri, in long threshold); + boolean checkExternalFreeStorage(in Uri fileUri); ObbInfo getObbInfo(in String filename); long calculateDirectorySize(in String directory); } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 2e56996..ba2f5d4 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -30,7 +30,6 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.PatternMatcher; -import android.util.Config; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -238,7 +237,7 @@ public class ResolverActivity extends AlertActivity implements ResolveInfo r0 = rList.get(0); for (int i=1; i<N; i++) { ResolveInfo ri = rList.get(i); - if (Config.LOGV) Log.v( + if (false) Log.v( "ResolveListActivity", r0.activityInfo.name + "=" + r0.priority + "/" + r0.isDefault + " vs " + diff --git a/core/java/com/android/internal/content/NativeLibraryHelper.java b/core/java/com/android/internal/content/NativeLibraryHelper.java index 4ae55fc..9ae7def 100644 --- a/core/java/com/android/internal/content/NativeLibraryHelper.java +++ b/core/java/com/android/internal/content/NativeLibraryHelper.java @@ -4,7 +4,6 @@ import android.content.pm.PackageManager; import android.os.Build; import android.os.FileUtils; import android.os.SystemProperties; -import android.util.Config; import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -176,7 +175,7 @@ public class NativeLibraryHelper { continue; } - if (Config.LOGD) { + if (false) { Log.d(TAG, "Found gdbserver: " + entry.getName()); } diff --git a/core/java/com/android/internal/content/PackageHelper.java b/core/java/com/android/internal/content/PackageHelper.java index d6c43f9..b57046c 100644 --- a/core/java/com/android/internal/content/PackageHelper.java +++ b/core/java/com/android/internal/content/PackageHelper.java @@ -56,18 +56,13 @@ public class PackageHelper { return null; } - public static String createSdDir(long sizeBytes, String cid, + public static String createSdDir(int sizeMb, String cid, String sdEncKey, int uid) { // Create mount point via MountService IMountService mountService = getMountService(); - int sizeMb = (int) (sizeBytes >> 20); - if ((sizeBytes - (sizeMb * 1024 * 1024)) > 0) { - sizeMb++; - } - // Add buffer size - sizeMb++; + if (localLOGV) - Log.i(TAG, "Size of container " + sizeMb + " MB " + sizeBytes + " bytes"); + Log.i(TAG, "Size of container " + sizeMb + " MB"); try { int rc = mountService.createSecureContainer( diff --git a/core/java/com/android/internal/net/DomainNameValidator.java b/core/java/com/android/internal/net/DomainNameValidator.java index 36973f1..3950655 100644 --- a/core/java/com/android/internal/net/DomainNameValidator.java +++ b/core/java/com/android/internal/net/DomainNameValidator.java @@ -16,7 +16,6 @@ package com.android.internal.net; import android.net.NetworkUtils; -import android.util.Config; import android.util.Log; import java.net.InetAddress; @@ -35,7 +34,7 @@ public class DomainNameValidator { private final static String TAG = "DomainNameValidator"; private static final boolean DEBUG = false; - private static final boolean LOG_ENABLED = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean LOG_ENABLED = false; private static final int ALT_DNS_NAME = 2; private static final int ALT_IPA_NAME = 7; diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 2847cf3..12687a1 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -36,6 +36,7 @@ import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.TelephonyManager; import android.util.Log; +import android.util.LogWriter; import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.Slog; @@ -70,7 +71,7 @@ public final class BatteryStatsImpl extends BatteryStats { private static final int MAGIC = 0xBA757475; // 'BATSTATS' // Current on-disk Parcel version - private static final int VERSION = 54; + private static final int VERSION = 60; // Maximum number of items we will record in the history. private static final int MAX_HISTORY_ITEMS = 2000; @@ -154,11 +155,27 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mHaveBatteryLevel = false; boolean mRecordingHistory = true; int mNumHistoryItems; + + static final int MAX_HISTORY_BUFFER = 128*1024; // 128KB + static final int MAX_MAX_HISTORY_BUFFER = 144*1024; // 144KB + final Parcel mHistoryBuffer = Parcel.obtain(); + final HistoryItem mHistoryLastWritten = new HistoryItem(); + final HistoryItem mHistoryLastLastWritten = new HistoryItem(); + final HistoryItem mHistoryReadTmp = new HistoryItem(); + int mHistoryBufferLastPos = -1; + boolean mHistoryOverflow = false; + long mLastHistoryTime = 0; + + final HistoryItem mHistoryCur = new HistoryItem(); + HistoryItem mHistory; HistoryItem mHistoryEnd; HistoryItem mHistoryLastEnd; HistoryItem mHistoryCache; - final HistoryItem mHistoryCur = new HistoryItem(); + + private HistoryItem mHistoryIterator; + private boolean mReadOverflow; + private boolean mIteratingHistory; int mStartCount; @@ -1189,9 +1206,84 @@ public final class BatteryStatsImpl extends BatteryStats { mBtHeadset = headset; } + int mChangedBufferStates = 0; + + void addHistoryBufferLocked(long curTime) { + if (!mHaveBatteryLevel || !mRecordingHistory) { + return; + } + + final long timeDiff = (mHistoryBaseTime+curTime) - mHistoryLastWritten.time; + if (mHistoryBufferLastPos >= 0 && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE + && timeDiff < 2000 + && ((mHistoryLastWritten.states^mHistoryCur.states)&mChangedBufferStates) == 0) { + // If the current is the same as the one before, then we no + // longer need the entry. + mHistoryBuffer.setDataSize(mHistoryBufferLastPos); + mHistoryBuffer.setDataPosition(mHistoryBufferLastPos); + mHistoryBufferLastPos = -1; + if (mHistoryLastLastWritten.cmd == HistoryItem.CMD_UPDATE + && timeDiff < 500 && mHistoryLastLastWritten.same(mHistoryCur)) { + // If this results in us returning to the state written + // prior to the last one, then we can just delete the last + // written one and drop the new one. Nothing more to do. + mHistoryLastWritten.setTo(mHistoryLastLastWritten); + mHistoryLastLastWritten.cmd = HistoryItem.CMD_NULL; + return; + } + mChangedBufferStates |= mHistoryLastWritten.states^mHistoryCur.states; + curTime = mHistoryLastWritten.time - mHistoryBaseTime; + mHistoryLastWritten.setTo(mHistoryLastLastWritten); + } else { + mChangedBufferStates = 0; + } + + final int dataSize = mHistoryBuffer.dataSize(); + if (dataSize >= MAX_HISTORY_BUFFER) { + if (!mHistoryOverflow) { + mHistoryOverflow = true; + addHistoryBufferLocked(curTime, HistoryItem.CMD_OVERFLOW); + } + + // Once we've reached the maximum number of items, we only + // record changes to the battery level and the most interesting states. + // Once we've reached the maximum maximum number of items, we only + // record changes to the battery level. + if (mHistoryLastWritten.batteryLevel == mHistoryCur.batteryLevel && + (dataSize >= MAX_MAX_HISTORY_BUFFER + || ((mHistoryEnd.states^mHistoryCur.states) + & HistoryItem.MOST_INTERESTING_STATES) == 0)) { + return; + } + } + + addHistoryBufferLocked(curTime, HistoryItem.CMD_UPDATE); + } + + void addHistoryBufferLocked(long curTime, byte cmd) { + int origPos = 0; + if (mIteratingHistory) { + origPos = mHistoryBuffer.dataPosition(); + mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); + } + mHistoryBufferLastPos = mHistoryBuffer.dataPosition(); + mHistoryLastLastWritten.setTo(mHistoryLastWritten); + mHistoryLastWritten.setTo(mHistoryBaseTime + curTime, cmd, mHistoryCur); + mHistoryLastWritten.writeDelta(mHistoryBuffer, mHistoryLastLastWritten); + mLastHistoryTime = curTime; + if (DEBUG_HISTORY) Slog.i(TAG, "Writing history buffer: was " + mHistoryBufferLastPos + + " now " + mHistoryBuffer.dataPosition() + + " size is now " + mHistoryBuffer.dataSize()); + if (mIteratingHistory) { + mHistoryBuffer.setDataPosition(origPos); + } + } + int mChangedStates = 0; void addHistoryRecordLocked(long curTime) { + addHistoryBufferLocked(curTime); + if (!mHaveBatteryLevel || !mRecordingHistory) { return; } @@ -1206,6 +1298,7 @@ public final class BatteryStatsImpl extends BatteryStats { // If the current is the same as the one before, then we no // longer need the entry. if (mHistoryLastEnd != null && mHistoryLastEnd.cmd == HistoryItem.CMD_UPDATE + && (mHistoryBaseTime+curTime) < (mHistoryEnd.time+500) && mHistoryLastEnd.same(mHistoryCur)) { mHistoryLastEnd.next = null; mHistoryEnd.next = mHistoryCache; @@ -1268,6 +1361,7 @@ public final class BatteryStatsImpl extends BatteryStats { } void clearHistoryLocked() { + if (DEBUG_HISTORY) Slog.i(TAG, "********** CLEARING HISTORY!"); if (mHistory != null) { mHistoryEnd.next = mHistoryCache; mHistoryCache = mHistory; @@ -1275,6 +1369,15 @@ public final class BatteryStatsImpl extends BatteryStats { } mNumHistoryItems = 0; mHistoryBaseTime = 0; + mLastHistoryTime = 0; + + mHistoryBuffer.setDataSize(0); + mHistoryBuffer.setDataPosition(0); + mHistoryBuffer.setDataCapacity(MAX_HISTORY_BUFFER/2); + mHistoryLastLastWritten.cmd = HistoryItem.CMD_NULL; + mHistoryLastWritten.cmd = HistoryItem.CMD_NULL; + mHistoryBufferLastPos = -1; + mHistoryOverflow = false; } public void doUnplugLocked(long batteryUptime, long batteryRealtime) { @@ -3910,11 +4013,13 @@ public final class BatteryStatsImpl extends BatteryStats { mDischargeUnplugLevel = 0; mDischargeCurrentLevel = 0; initDischarge(); + clearHistoryLocked(); } public BatteryStatsImpl(Parcel p) { mFile = null; mHandler = null; + clearHistoryLocked(); readFromParcel(p); } @@ -3932,25 +4037,84 @@ public final class BatteryStatsImpl extends BatteryStats { } } - private HistoryItem mHistoryIterator; - - public boolean startIteratingHistoryLocked() { + @Override + public boolean startIteratingOldHistoryLocked() { + if (DEBUG_HISTORY) Slog.i(TAG, "ITERATING: buff size=" + mHistoryBuffer.dataSize() + + " pos=" + mHistoryBuffer.dataPosition()); + mHistoryBuffer.setDataPosition(0); + mHistoryReadTmp.clear(); + mReadOverflow = false; + mIteratingHistory = true; return (mHistoryIterator = mHistory) != null; } - public boolean getNextHistoryLocked(HistoryItem out) { + @Override + public boolean getNextOldHistoryLocked(HistoryItem out) { + boolean end = mHistoryBuffer.dataPosition() >= mHistoryBuffer.dataSize(); + if (!end) { + mHistoryReadTmp.readDelta(mHistoryBuffer); + mReadOverflow |= mHistoryReadTmp.cmd == HistoryItem.CMD_OVERFLOW; + } HistoryItem cur = mHistoryIterator; if (cur == null) { + if (!mReadOverflow && !end) { + Slog.w(TAG, "Old history ends before new history!"); + } return false; } out.setTo(cur); mHistoryIterator = cur.next; + if (!mReadOverflow) { + if (end) { + Slog.w(TAG, "New history ends before old history!"); + } else if (!out.same(mHistoryReadTmp)) { + long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); + PrintWriter pw = new PrintWriter(new LogWriter(android.util.Log.WARN, TAG)); + pw.println("Histories differ!"); + pw.println("Old history:"); + (new HistoryPrinter()).printNextItem(pw, out, now); + pw.println("New history:"); + (new HistoryPrinter()).printNextItem(pw, mHistoryReadTmp, now); + } + } return true; } @Override - public HistoryItem getHistory() { - return mHistory; + public void finishIteratingOldHistoryLocked() { + mIteratingHistory = false; + mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); + } + + @Override + public boolean startIteratingHistoryLocked() { + if (DEBUG_HISTORY) Slog.i(TAG, "ITERATING: buff size=" + mHistoryBuffer.dataSize() + + " pos=" + mHistoryBuffer.dataPosition()); + mHistoryBuffer.setDataPosition(0); + mReadOverflow = false; + mIteratingHistory = true; + return mHistoryBuffer.dataSize() > 0; + } + + @Override + public boolean getNextHistoryLocked(HistoryItem out) { + final int pos = mHistoryBuffer.dataPosition(); + if (pos == 0) { + out.clear(); + } + boolean end = pos >= mHistoryBuffer.dataSize(); + if (end) { + return false; + } + + out.readDelta(mHistoryBuffer); + return true; + } + + @Override + public void finishIteratingHistoryLocked() { + mIteratingHistory = false; + mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); } @Override @@ -4697,7 +4861,9 @@ public final class BatteryStatsImpl extends BatteryStats { Slog.e("BatteryStats", "Error reading battery statistics", e); } - addHistoryRecordLocked(SystemClock.elapsedRealtime(), HistoryItem.CMD_START); + long now = SystemClock.elapsedRealtime(); + addHistoryRecordLocked(now, HistoryItem.CMD_START); + addHistoryBufferLocked(now, HistoryItem.CMD_START); } public int describeContents() { @@ -4705,30 +4871,54 @@ public final class BatteryStatsImpl extends BatteryStats { } void readHistory(Parcel in) { - mHistory = mHistoryEnd = mHistoryCache = null; - mHistoryBaseTime = 0; - long time; - while (in.dataAvail() > 0 && (time=in.readLong()) >= 0) { - HistoryItem rec = new HistoryItem(time, in); - addHistoryRecordLocked(rec); - if (rec.time > mHistoryBaseTime) { - mHistoryBaseTime = rec.time; - } + mHistoryBaseTime = in.readLong(); + + mHistoryBuffer.setDataSize(0); + mHistoryBuffer.setDataPosition(0); + + int bufSize = in.readInt(); + int curPos = in.dataPosition(); + if (bufSize >= (MAX_MAX_HISTORY_BUFFER*3)) { + Slog.w(TAG, "File corrupt: history data buffer too large " + bufSize); + } else if ((bufSize&~3) != bufSize) { + Slog.w(TAG, "File corrupt: history data buffer not aligned " + bufSize); + } else { + if (DEBUG_HISTORY) Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize + + " bytes at " + curPos); + mHistoryBuffer.appendFrom(in, curPos, bufSize); + in.setDataPosition(curPos + bufSize); } - long oldnow = SystemClock.elapsedRealtime() - (5*60*100); + long oldnow = SystemClock.elapsedRealtime() - (5*60*1000); if (oldnow > 0) { // If the system process has restarted, but not the entire // system, then the mHistoryBaseTime already accounts for // much of the elapsed time. We thus want to adjust it back, // to avoid large gaps in the data. We determine we are // in this case by arbitrarily saying it is so if at this - // point in boot the elapsed time is already more than 5 seconds. + // point in boot the elapsed time is already more than 5 minutes. mHistoryBaseTime -= oldnow; } } + void readOldHistory(Parcel in) { + mHistory = mHistoryEnd = mHistoryCache = null; + long time; + while (in.dataAvail() > 0 && (time=in.readLong()) >= 0) { + HistoryItem rec = new HistoryItem(time, in); + addHistoryRecordLocked(rec); + } + } + void writeHistory(Parcel out) { + out.writeLong(mLastHistoryTime); + out.writeInt(mHistoryBuffer.dataSize()); + if (DEBUG_HISTORY) Slog.i(TAG, "***************** WRITING HISTORY: " + + mHistoryBuffer.dataSize() + " bytes at " + out.dataPosition()); + out.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); + } + + void writeOldHistory(Parcel out) { HistoryItem rec = mHistory; while (rec != null) { if (rec.time >= 0) rec.writeToParcel(out, 0); @@ -4746,6 +4936,7 @@ public final class BatteryStatsImpl extends BatteryStats { } readHistory(in); + readOldHistory(in); mStartCount = in.readInt(); mBatteryUptime = in.readLong(); @@ -4935,6 +5126,9 @@ public final class BatteryStatsImpl extends BatteryStats { * @param out the Parcel to be written to. */ public void writeSummaryToParcel(Parcel out) { + // Need to update with current kernel wake lock counts. + updateKernelWakelocksLocked(); + final long NOW_SYS = SystemClock.uptimeMillis() * 1000; final long NOWREAL_SYS = SystemClock.elapsedRealtime() * 1000; final long NOW = getBatteryUptimeLocked(NOW_SYS); @@ -4943,6 +5137,7 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeInt(VERSION); writeHistory(out); + writeOldHistory(out); out.writeInt(mStartCount); out.writeLong(computeBatteryUptime(NOW_SYS, STATS_SINCE_CHARGED)); @@ -5256,6 +5451,9 @@ public final class BatteryStatsImpl extends BatteryStats { @SuppressWarnings("unused") void writeToParcelLocked(Parcel out, boolean inclUids, int flags) { + // Need to update with current kernel wake lock counts. + updateKernelWakelocksLocked(); + final long uSecUptime = SystemClock.uptimeMillis() * 1000; final long uSecRealtime = SystemClock.elapsedRealtime() * 1000; final long batteryUptime = getBatteryUptimeLocked(uSecUptime); @@ -5358,6 +5556,11 @@ public final class BatteryStatsImpl extends BatteryStats { } }; + public void prepareForDumpLocked() { + // Need to retrieve current kernel wake lock stats before printing. + updateKernelWakelocksLocked(); + } + public void dumpLocked(PrintWriter pw) { if (DEBUG) { Printer pr = new PrintWriterPrinter(pw); diff --git a/core/java/com/android/internal/os/BinderInternal.java b/core/java/com/android/internal/os/BinderInternal.java index ba0bf0d..f54a3e9 100644 --- a/core/java/com/android/internal/os/BinderInternal.java +++ b/core/java/com/android/internal/os/BinderInternal.java @@ -19,7 +19,6 @@ package com.android.internal.os; import android.os.Binder; import android.os.IBinder; import android.os.SystemClock; -import android.util.Config; import android.util.EventLog; import android.util.Log; diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java index f58f261..0f086f6 100644 --- a/core/java/com/android/internal/os/RuntimeInit.java +++ b/core/java/com/android/internal/os/RuntimeInit.java @@ -23,7 +23,6 @@ import android.os.Debug; import android.os.IBinder; import android.os.Process; import android.os.SystemProperties; -import android.util.Config; import android.util.Log; import android.util.Slog; @@ -90,14 +89,14 @@ public class RuntimeInit { } private static final void commonInit() { - if (Config.LOGV) Slog.d(TAG, "Entered RuntimeInit!"); + if (false) Slog.d(TAG, "Entered RuntimeInit!"); /* set default handler; this applies to all threads in the VM */ Thread.setDefaultUncaughtExceptionHandler(new UncaughtHandler()); int hasQwerty = getQwertyKeyboard(); - if (Config.LOGV) Slog.d(TAG, ">>>>> qwerty keyboard = " + hasQwerty); + if (false) Slog.d(TAG, ">>>>> qwerty keyboard = " + hasQwerty); if (hasQwerty == 1) { System.setProperty("qwerty", "1"); } @@ -234,7 +233,7 @@ public class RuntimeInit { */ finishInit(); - if (Config.LOGV) Slog.d(TAG, "Leaving RuntimeInit!"); + if (false) Slog.d(TAG, "Leaving RuntimeInit!"); } public static final native void finishInit(); diff --git a/core/java/com/android/internal/os/SamplingProfilerIntegration.java b/core/java/com/android/internal/os/SamplingProfilerIntegration.java index 8c256e0..df0fcd9 100644 --- a/core/java/com/android/internal/os/SamplingProfilerIntegration.java +++ b/core/java/com/android/internal/os/SamplingProfilerIntegration.java @@ -20,12 +20,15 @@ import android.content.pm.PackageInfo; import android.os.Build; import android.os.SystemProperties; import android.util.Log; -import dalvik.system.SamplingProfiler; +import dalvik.system.profiler.BinaryHprofWriter; +import dalvik.system.profiler.SamplingProfiler; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.PrintStream; +import java.util.Date; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; @@ -80,7 +83,8 @@ public class SamplingProfilerIntegration { } } - private static SamplingProfiler INSTANCE; + private static SamplingProfiler samplingProfiler; + private static long startMillis; /** * Is profiling enabled? @@ -96,10 +100,16 @@ public class SamplingProfilerIntegration { if (!enabled) { return; } + if (samplingProfiler != null) { + Log.e(TAG, "SamplingProfilerIntegration already started at " + new Date(startMillis)); + return; + } + ThreadGroup group = Thread.currentThread().getThreadGroup(); SamplingProfiler.ThreadSet threadSet = SamplingProfiler.newThreadGroupTheadSet(group); - INSTANCE = new SamplingProfiler(samplingProfilerDepth, threadSet); - INSTANCE.start(samplingProfilerMilliseconds); + samplingProfiler = new SamplingProfiler(samplingProfilerDepth, threadSet); + samplingProfiler.start(samplingProfilerMilliseconds); + startMillis = System.currentTimeMillis(); } /** @@ -109,6 +119,10 @@ public class SamplingProfilerIntegration { if (!enabled) { return; } + if (samplingProfiler == null) { + Log.e(TAG, "SamplingProfilerIntegration is not started"); + return; + } /* * If we're already writing a snapshot, don't bother enqueueing another @@ -137,8 +151,9 @@ public class SamplingProfilerIntegration { return; } writeSnapshotFile("zygote", null); - INSTANCE.shutdown(); - INSTANCE = null; + samplingProfiler.shutdown(); + samplingProfiler = null; + startMillis = 0; } /** @@ -148,40 +163,44 @@ public class SamplingProfilerIntegration { if (!enabled) { return; } - INSTANCE.stop(); + samplingProfiler.stop(); /* - * We use the current time as a unique ID. We can't use a counter - * because processes restart. This could result in some overlap if - * we capture two snapshots in rapid succession. + * We use the global start time combined with the process name + * as a unique ID. We can't use a counter because processes + * restart. This could result in some overlap if we capture + * two snapshots in rapid succession. */ - long start = System.currentTimeMillis(); String name = processName.replaceAll(":", "."); - String path = SNAPSHOT_DIR + "/" + name + "-" +System.currentTimeMillis() + ".snapshot"; - PrintStream out = null; + String path = SNAPSHOT_DIR + "/" + name + "-" + startMillis + ".snapshot"; + long start = System.currentTimeMillis(); + OutputStream outputStream = null; try { - out = new PrintStream(new BufferedOutputStream(new FileOutputStream(path))); + outputStream = new BufferedOutputStream(new FileOutputStream(path)); + PrintStream out = new PrintStream(outputStream); generateSnapshotHeader(name, packageInfo, out); - new SamplingProfiler.AsciiHprofWriter(INSTANCE.getHprofData(), out).write(); if (out.checkError()) { throw new IOException(); } + BinaryHprofWriter.write(samplingProfiler.getHprofData(), outputStream); } catch (IOException e) { Log.e(TAG, "Error writing snapshot to " + path, e); return; } finally { - IoUtils.closeQuietly(out); + IoUtils.closeQuietly(outputStream); } // set file readable to the world so that SamplingProfilerService // can put it to dropbox new File(path).setReadable(true, false); long elapsed = System.currentTimeMillis() - start; - Log.i(TAG, "Wrote snapshot for " + name + " in " + elapsed + "ms."); + Log.i(TAG, "Wrote snapshot " + path + " in " + elapsed + "ms."); + samplingProfiler.start(samplingProfilerMilliseconds); } /** - * generate header for snapshots, with the following format (like http header): + * generate header for snapshots, with the following format + * (like an HTTP header but without the \r): * * Version: <version number of profiler>\n * Process: <process name>\n @@ -194,7 +213,7 @@ public class SamplingProfilerIntegration { private static void generateSnapshotHeader(String processName, PackageInfo packageInfo, PrintStream out) { // profiler version - out.println("Version: 2"); + out.println("Version: 3"); out.println("Process: " + processName); if (packageInfo != null) { out.println("Package: " + packageInfo.packageName); diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index dea53bf..fbe66e5 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -25,13 +25,13 @@ import android.os.Debug; import android.os.FileUtils; import android.os.SystemClock; import android.os.SystemProperties; -import android.util.Config; import android.util.EventLog; import android.util.Log; import dalvik.system.VMRuntime; import dalvik.system.Zygote; -import dalvik.system.SamplingProfiler; + +import libcore.io.IoUtils; import java.io.BufferedReader; import java.io.FileDescriptor; @@ -99,25 +99,6 @@ public class ZygoteInit { private static final boolean PRELOAD_RESOURCES = true; /** - * List of methods we "warm up" in the register map cache. These were - * chosen because they appeared on the stack in GCs in multiple - * applications. - * - * This is in a VM-ready format, to minimize string processing. If a - * class is not already loaded, or a method is not found, the entry - * will be skipped. - * - * This doesn't really merit a separately-generated input file at this - * time. The list is fairly short, and the consequences of failure - * are minor. - */ - private static final String[] REGISTER_MAP_METHODS = { - // (currently not doing any) - //"Landroid/app/Activity;.setContentView:(I)V", - }; - - - /** * Invokes a static "main(argv[]) method on class "className". * Converts various failing exceptions into RuntimeExceptions, with * the assumption that they will then cause the VM instance to exit. @@ -274,7 +255,7 @@ public class ZygoteInit { runtime.setTargetHeapUtilization(0.8f); // Start with a clean slate. - runtime.gcSoftReferences(); + System.gc(); runtime.runFinalizationSync(); Debug.startAllocCounting(); @@ -292,16 +273,16 @@ public class ZygoteInit { } try { - if (Config.LOGV) { + if (false) { Log.v(TAG, "Preloading " + line + "..."); } Class.forName(line); if (Debug.getGlobalAllocSize() > PRELOAD_GC_THRESHOLD) { - if (Config.LOGV) { + if (false) { Log.v(TAG, " GC at " + Debug.getGlobalAllocSize()); } - runtime.gcSoftReferences(); + System.gc(); runtime.runFinalizationSync(); Debug.resetGlobalAllocSize(); } @@ -325,6 +306,7 @@ public class ZygoteInit { } catch (IOException e) { Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e); } finally { + IoUtils.closeQuietly(is); // Restore default. runtime.setTargetHeapUtilization(defaultUtilization); @@ -338,45 +320,6 @@ public class ZygoteInit { } /** - * Pre-caches register maps for methods that are commonly used. - */ - private static void cacheRegisterMaps() { - String failed = null; - int failure; - long startTime = System.nanoTime(); - - failure = 0; - - for (int i = 0; i < REGISTER_MAP_METHODS.length; i++) { - String str = REGISTER_MAP_METHODS[i]; - - if (!Debug.cacheRegisterMap(str)) { - if (failed == null) - failed = str; - failure++; - } - } - - long delta = System.nanoTime() - startTime; - - if (failure == REGISTER_MAP_METHODS.length) { - if (REGISTER_MAP_METHODS.length > 0) { - Log.i(TAG, - "Register map caching failed (precise GC not enabled?)"); - } - return; - } - - Log.i(TAG, "Register map cache: found " + - (REGISTER_MAP_METHODS.length - failure) + " of " + - REGISTER_MAP_METHODS.length + " methods in " + - (delta / 1000000L) + "ms"); - if (failure > 0) { - Log.i(TAG, " First failure: " + failed); - } - } - - /** * Load in commonly used resources, so they can be shared across * processes. * @@ -388,7 +331,7 @@ public class ZygoteInit { Debug.startAllocCounting(); try { - runtime.gcSoftReferences(); + System.gc(); runtime.runFinalizationSync(); mResources = Resources.getSystem(); mResources.startPreloading(); @@ -421,15 +364,15 @@ public class ZygoteInit { int N = ar.length(); for (int i=0; i<N; i++) { if (Debug.getGlobalAllocSize() > PRELOAD_GC_THRESHOLD) { - if (Config.LOGV) { + if (false) { Log.v(TAG, " GC at " + Debug.getGlobalAllocSize()); } - runtime.gcSoftReferences(); + System.gc(); runtime.runFinalizationSync(); Debug.resetGlobalAllocSize(); } int id = ar.getResourceId(i, 0); - if (Config.LOGV) { + if (false) { Log.v(TAG, "Preloading resource #" + Integer.toHexString(id)); } if (id != 0) { @@ -444,15 +387,15 @@ public class ZygoteInit { int N = ar.length(); for (int i=0; i<N; i++) { if (Debug.getGlobalAllocSize() > PRELOAD_GC_THRESHOLD) { - if (Config.LOGV) { + if (false) { Log.v(TAG, " GC at " + Debug.getGlobalAllocSize()); } - runtime.gcSoftReferences(); + System.gc(); runtime.runFinalizationSync(); Debug.resetGlobalAllocSize(); } int id = ar.getResourceId(i, 0); - if (Config.LOGV) { + if (false) { Log.v(TAG, "Preloading resource #" + Integer.toHexString(id)); } if (id != 0) { @@ -478,11 +421,11 @@ public class ZygoteInit { /* runFinalizationSync() lets finalizers be called in Zygote, * which doesn't have a HeapWorker thread. */ - runtime.gcSoftReferences(); + System.gc(); runtime.runFinalizationSync(); - runtime.gcSoftReferences(); + System.gc(); runtime.runFinalizationSync(); - runtime.gcSoftReferences(); + System.gc(); runtime.runFinalizationSync(); } @@ -564,7 +507,6 @@ public class ZygoteInit { EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START, SystemClock.uptimeMillis()); preloadClasses(); - //cacheRegisterMaps(); preloadResources(); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END, SystemClock.uptimeMillis()); diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 7f23ed5..7d21489 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -34,5 +34,6 @@ oneway interface IStatusBar void setMenuKeyVisible(boolean visible); void setImeWindowStatus(in IBinder token, int vis, int backDisposition); void setHardKeyboardStatus(boolean available, boolean enabled); + void userActivity(); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index d6ca426..bfc717b 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -46,4 +46,5 @@ interface IStatusBarService void onNotificationClear(String pkg, String tag, int id); void setSystemUiVisibility(int vis); void setHardKeyboardEnabled(boolean enabled); + void userActivity(); } diff --git a/core/java/com/android/internal/view/IInputConnectionWrapper.java b/core/java/com/android/internal/view/IInputConnectionWrapper.java index b5df812..c792d78 100644 --- a/core/java/com/android/internal/view/IInputConnectionWrapper.java +++ b/core/java/com/android/internal/view/IInputConnectionWrapper.java @@ -174,7 +174,7 @@ public class IInputConnectionWrapper extends IInputContext.Stub { public void performPrivateCommand(String action, Bundle data) { dispatchMessage(obtainMessageOO(DO_PERFORM_PRIVATE_COMMAND, action, data)); } - + void dispatchMessage(Message msg) { // If we are calling this from the main thread, then we can call // right through. Otherwise, we need to send the message to the diff --git a/core/java/com/android/internal/view/IInputContext.aidl b/core/java/com/android/internal/view/IInputContext.aidl index e00dd4e..719a24f 100644 --- a/core/java/com/android/internal/view/IInputContext.aidl +++ b/core/java/com/android/internal/view/IInputContext.aidl @@ -72,4 +72,5 @@ import com.android.internal.view.IInputContextCallback; void setComposingRegion(int start, int end); void getSelectedText(int flags, int seq, IInputContextCallback callback); + } diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 611d987..4ffa4e1 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -33,6 +33,7 @@ interface IInputMethodManager { List<InputMethodInfo> getEnabledInputMethodList(); List<InputMethodSubtype> getEnabledInputMethodSubtypeList(in InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes); + InputMethodSubtype getLastInputMethodSubtype(); // TODO: We should change the return type from List to List<Parcelable> // Currently there is a bug that aidl doesn't accept List<Parcelable> List getShortcutInputMethodsAndSubtypes(); diff --git a/core/java/com/android/internal/view/InputConnectionWrapper.java b/core/java/com/android/internal/view/InputConnectionWrapper.java index b13118a..a235d9a 100644 --- a/core/java/com/android/internal/view/InputConnectionWrapper.java +++ b/core/java/com/android/internal/view/InputConnectionWrapper.java @@ -251,7 +251,7 @@ public class InputConnectionWrapper implements InputConnection { } return value; } - + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { ExtractedText value = null; try { diff --git a/core/java/com/android/internal/view/StandaloneActionMode.java b/core/java/com/android/internal/view/StandaloneActionMode.java index 2d067da..b54daba 100644 --- a/core/java/com/android/internal/view/StandaloneActionMode.java +++ b/core/java/com/android/internal/view/StandaloneActionMode.java @@ -135,6 +135,6 @@ public class StandaloneActionMode extends ActionMode implements MenuBuilder.Call public void onMenuModeChange(MenuBuilder menu) { invalidate(); - mContextView.openOverflowMenu(); + mContextView.showOverflowMenu(); } } diff --git a/core/java/com/android/internal/view/menu/ActionMenuItemView.java b/core/java/com/android/internal/view/menu/ActionMenuItemView.java index 3325df6..beacf75 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ActionMenuItemView.java @@ -28,7 +28,7 @@ import android.widget.LinearLayout; * @hide */ public class ActionMenuItemView extends LinearLayout - implements MenuView.ItemView, View.OnClickListener { + implements MenuView.ItemView, View.OnClickListener, ActionMenuView.ActionMenuChildView { private static final String TAG = "ActionMenuItemView"; private MenuItemImpl mItemData; @@ -56,6 +56,7 @@ public class ActionMenuItemView extends LinearLayout mTextButton = (Button) findViewById(com.android.internal.R.id.textButton); mImageButton.setOnClickListener(this); mTextButton.setOnClickListener(this); + setOnClickListener(this); } public MenuItemImpl getItemData() { @@ -136,4 +137,12 @@ public class ActionMenuItemView extends LinearLayout public boolean showsIcon() { return true; } + + public boolean needsDividerBefore() { + return hasText() && mItemData.getIcon() == null; + } + + public boolean needsDividerAfter() { + return hasText(); + } } diff --git a/core/java/com/android/internal/view/menu/ActionMenuPresenter.java b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java new file mode 100644 index 0000000..0051ec3 --- /dev/null +++ b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view.menu; + +import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.SparseBooleanArray; +import android.view.MenuItem; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.widget.ImageButton; + +import java.util.ArrayList; + +/** + * MenuPresenter for building action menus as seen in the action bar and action modes. + */ +public class ActionMenuPresenter extends BaseMenuPresenter { + private static final String TAG = "ActionMenuPresenter"; + + private View mOverflowButton; + private boolean mReserveOverflow; + private int mWidthLimit; + private int mActionItemWidthLimit; + private int mMaxItems; + private boolean mStrictWidthLimit; + + // Group IDs that have been added as actions - used temporarily, allocated here for reuse. + private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray(); + + private View mScrapActionButtonView; + + private OverflowPopup mOverflowPopup; + private ActionButtonSubmenu mActionButtonPopup; + + private OpenOverflowRunnable mPostedOpenRunnable; + + public ActionMenuPresenter() { + super(com.android.internal.R.layout.action_menu_layout, + com.android.internal.R.layout.action_menu_item_layout); + } + + @Override + public void initForMenu(Context context, MenuBuilder menu) { + super.initForMenu(context, menu); + + final Resources res = context.getResources(); + final int screen = res.getConfiguration().screenLayout; + // TODO Use the no-buttons specifier instead here + mReserveOverflow = (screen & Configuration.SCREENLAYOUT_SIZE_MASK) == + Configuration.SCREENLAYOUT_SIZE_XLARGE; + mWidthLimit = res.getDisplayMetrics().widthPixels / 2; + + // Measure for initial configuration + mMaxItems = res.getInteger(com.android.internal.R.integer.max_action_buttons); + + int width = mWidthLimit; + if (mReserveOverflow) { + if (mOverflowButton == null) { + mOverflowButton = new OverflowMenuButton(mContext); + final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + mOverflowButton.measure(spec, spec); + } + width -= mOverflowButton.getMeasuredWidth(); + } else { + mOverflowButton = null; + } + + mActionItemWidthLimit = width; + + // Drop a scrap view as it may no longer reflect the proper context/config. + mScrapActionButtonView = null; + } + + public void setWidthLimit(int width, boolean strict) { + if (mReserveOverflow) { + width -= mOverflowButton.getMeasuredWidth(); + } + mActionItemWidthLimit = width; + mStrictWidthLimit = strict; + } + + public void setItemLimit(int itemCount) { + mMaxItems = itemCount; + } + + @Override + public MenuView getMenuView(ViewGroup root) { + MenuView result = super.getMenuView(root); + ((ActionMenuView) result).setPresenter(this); + return result; + } + + @Override + public View getItemView(MenuItemImpl item, View convertView, ViewGroup parent) { + final View actionView = item.getActionView(); + return actionView != null ? actionView : super.getItemView(item, convertView, parent); + } + + @Override + public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) { + itemView.initialize(item, 0); + ((ActionMenuItemView) itemView).setItemInvoker((ActionMenuView) mMenuView); + } + + @Override + public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) { + return item.isActionButton(); + } + + @Override + public void updateMenuView(boolean cleared) { + super.updateMenuView(cleared); + + if (mReserveOverflow && mMenu.getNonActionItems().size() > 0) { + if (mOverflowButton == null) { + mOverflowButton = new OverflowMenuButton(mContext); + mOverflowButton.setLayoutParams( + ((ActionMenuView) mMenuView).generateOverflowButtonLayoutParams()); + } + ViewGroup parent = (ViewGroup) mOverflowButton.getParent(); + if (parent != mMenuView) { + if (parent != null) { + parent.removeView(mOverflowButton); + } + ((ViewGroup) mMenuView).addView(mOverflowButton); + } + } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) { + ((ViewGroup) mMenuView).removeView(mOverflowButton); + } + } + + @Override + public boolean filterLeftoverView(ViewGroup parent, int childIndex) { + if (parent.getChildAt(childIndex) == mOverflowButton) return false; + return super.filterLeftoverView(parent, childIndex); + } + + public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + if (!subMenu.hasVisibleItems()) return false; + + SubMenuBuilder topSubMenu = subMenu; + while (topSubMenu.getParentMenu() != mMenu) { + topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu(); + } + View anchor = findViewForItem(topSubMenu.getItem()); + if (anchor == null) return false; + + mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu); + mActionButtonPopup.setAnchorView(anchor); + mActionButtonPopup.show(); + super.onSubMenuSelected(subMenu); + return true; + } + + private View findViewForItem(MenuItem item) { + final ViewGroup parent = (ViewGroup) mMenuView; + if (parent == null) return null; + + final int count = parent.getChildCount(); + for (int i = 0; i < count; i++) { + final View child = parent.getChildAt(i); + if (child instanceof MenuView.ItemView && + ((MenuView.ItemView) child).getItemData() == item) { + return child; + } + } + return null; + } + + /** + * Display the overflow menu if one is present. + * @return true if the overflow menu was shown, false otherwise. + */ + public boolean showOverflowMenu() { + if (mReserveOverflow && !isOverflowMenuShowing() && mMenuView != null && + mPostedOpenRunnable == null) { + OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true); + mPostedOpenRunnable = new OpenOverflowRunnable(popup); + // Post this for later; we might still need a layout for the anchor to be right. + ((View) mMenuView).post(mPostedOpenRunnable); + + // ActionMenuPresenter uses null as a callback argument here + // to indicate overflow is opening. + super.onSubMenuSelected(null); + + return true; + } + return false; + } + + /** + * Hide the overflow menu if it is currently showing. + * + * @return true if the overflow menu was hidden, false otherwise. + */ + public boolean hideOverflowMenu() { + if (mPostedOpenRunnable != null && mMenuView != null) { + ((View) mMenuView).removeCallbacks(mPostedOpenRunnable); + return true; + } + + MenuPopupHelper popup = mOverflowPopup; + if (popup != null) { + popup.dismiss(); + return true; + } + return false; + } + + /** + * Dismiss all popup menus - overflow and submenus. + * @return true if popups were dismissed, false otherwise. (This can be because none were open.) + */ + public boolean dismissPopupMenus() { + boolean result = hideOverflowMenu(); + result |= hideSubMenus(); + return result; + } + + /** + * Dismiss all submenu popups. + * + * @return true if popups were dismissed, false otherwise. (This can be because none were open.) + */ + public boolean hideSubMenus() { + if (mActionButtonPopup != null) { + mActionButtonPopup.dismiss(); + return true; + } + return false; + } + + /** + * @return true if the overflow menu is currently showing + */ + public boolean isOverflowMenuShowing() { + return mOverflowPopup != null && mOverflowPopup.isShowing(); + } + + /** + * @return true if space has been reserved in the action menu for an overflow item. + */ + public boolean isOverflowReserved() { + return mReserveOverflow; + } + + public boolean flagActionItems() { + final ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems(); + final int itemsSize = visibleItems.size(); + int maxActions = mMaxItems; + int widthLimit = mActionItemWidthLimit; + final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final ViewGroup parent = (ViewGroup) mMenuView; + + int requiredItems = 0; + int requestedItems = 0; + int firstActionWidth = 0; + boolean hasOverflow = false; + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + if (item.requiresActionButton()) { + requiredItems++; + } else if (item.requestsActionButton()) { + requestedItems++; + } else { + hasOverflow = true; + } + } + + // Reserve a spot for the overflow item if needed. + if (mReserveOverflow && + (hasOverflow || requiredItems + requestedItems > maxActions)) { + maxActions--; + } + maxActions -= requiredItems; + + final SparseBooleanArray seenGroups = mActionButtonGroups; + seenGroups.clear(); + + // Flag as many more requested items as will fit. + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + + if (item.requiresActionButton()) { + View v = item.getActionView(); + if (v == null) { + v = getItemView(item, mScrapActionButtonView, parent); + if (mScrapActionButtonView == null) { + mScrapActionButtonView = v; + } + } + v.measure(querySpec, querySpec); + final int measuredWidth = v.getMeasuredWidth(); + widthLimit -= measuredWidth; + if (firstActionWidth == 0) { + firstActionWidth = measuredWidth; + } + final int groupId = item.getGroupId(); + if (groupId != 0) { + seenGroups.put(groupId, true); + } + } else if (item.requestsActionButton()) { + // Items in a group with other items that already have an action slot + // can break the max actions rule, but not the width limit. + final int groupId = item.getGroupId(); + final boolean inGroup = seenGroups.get(groupId); + boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0; + maxActions--; + + if (isAction) { + View v = item.getActionView(); + if (v == null) { + v = getItemView(item, mScrapActionButtonView, parent); + if (mScrapActionButtonView == null) { + mScrapActionButtonView = v; + } + } + v.measure(querySpec, querySpec); + final int measuredWidth = v.getMeasuredWidth(); + widthLimit -= measuredWidth; + if (firstActionWidth == 0) { + firstActionWidth = measuredWidth; + } + + if (mStrictWidthLimit) { + isAction = widthLimit >= 0; + } else { + // Did this push the entire first item past the limit? + isAction = widthLimit + firstActionWidth > 0; + } + } + + if (isAction && groupId != 0) { + seenGroups.put(groupId, true); + } else if (inGroup) { + // We broke the width limit. Demote the whole group, they all overflow now. + seenGroups.put(groupId, false); + for (int j = 0; j < i; j++) { + MenuItemImpl areYouMyGroupie = visibleItems.get(j); + if (areYouMyGroupie.getGroupId() == groupId) { + areYouMyGroupie.setIsActionButton(false); + } + } + } + + item.setIsActionButton(isAction); + } + } + return true; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + dismissPopupMenus(); + super.onCloseMenu(menu, allMenusAreClosing); + } + + private class OverflowMenuButton extends ImageButton implements ActionMenuChildView { + public OverflowMenuButton(Context context) { + super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle); + + setClickable(true); + setFocusable(true); + setVisibility(VISIBLE); + setEnabled(true); + } + + @Override + public boolean performClick() { + if (super.performClick()) { + return true; + } + + playSoundEffect(SoundEffectConstants.CLICK); + showOverflowMenu(); + return true; + } + + public boolean needsDividerBefore() { + return true; + } + + public boolean needsDividerAfter() { + return false; + } + } + + private class OverflowPopup extends MenuPopupHelper { + public OverflowPopup(Context context, MenuBuilder menu, View anchorView, + boolean overflowOnly) { + super(context, menu, anchorView, overflowOnly); + } + + @Override + public void onDismiss() { + super.onDismiss(); + mMenu.close(); + mOverflowPopup = null; + } + } + + private class ActionButtonSubmenu extends MenuPopupHelper { + private SubMenuBuilder mSubMenu; + + public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu) { + super(context, subMenu); + mSubMenu = subMenu; + + MenuItemImpl item = (MenuItemImpl) subMenu.getItem(); + if (!item.isActionButton()) { + // Give a reasonable anchor to nested submenus. + setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton); + } + } + + @Override + public void onDismiss() { + super.onDismiss(); + mSubMenu.close(); + mActionButtonPopup = null; + } + } + + private class OpenOverflowRunnable implements Runnable { + private OverflowPopup mPopup; + + public OpenOverflowRunnable(OverflowPopup popup) { + mPopup = popup; + } + + public void run() { + mMenu.changeMenuMode(); + if (mPopup.tryShow()) { + mOverflowPopup = mPopup; + mPostedOpenRunnable = null; + } + } + } +} diff --git a/core/java/com/android/internal/view/menu/ActionMenuView.java b/core/java/com/android/internal/view/menu/ActionMenuView.java index 5da5e44..290bf08 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuView.java +++ b/core/java/com/android/internal/view/menu/ActionMenuView.java @@ -17,63 +17,25 @@ package com.android.internal.view.menu; import android.content.Context; import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.Gravity; -import android.view.SoundEffectConstants; import android.view.View; +import android.view.ViewDebug; import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; import android.widget.LinearLayout; -import java.util.ArrayList; - /** * @hide */ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView { private static final String TAG = "ActionMenuView"; - - // TODO Theme/style this. - private static final int DIVIDER_PADDING = 12; // dips private MenuBuilder mMenu; - private int mMaxItems; - private int mWidthLimit; private boolean mReserveOverflow; - private OverflowMenuButton mOverflowButton; - private MenuPopupHelper mOverflowPopup; - - private float mDividerPadding; - - private Drawable mDivider; - - private final Runnable mShowOverflow = new Runnable() { - public void run() { - showOverflowMenu(); - } - }; - - private class OpenOverflowRunnable implements Runnable { - private MenuPopupHelper mPopup; - - public OpenOverflowRunnable(MenuPopupHelper popup) { - mPopup = popup; - } - - public void run() { - if (mPopup.tryShow()) { - mOverflowPopup = mPopup; - mPostedOpenRunnable = null; - } - } - } - - private OpenOverflowRunnable mPostedOpenRunnable; + private ActionMenuPresenter mPresenter; + private boolean mUpdateContentsBeforeMeasure; + private boolean mFormatItems; public ActionMenuView(Context context) { this(context, null); @@ -81,57 +43,117 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo public ActionMenuView(Context context, AttributeSet attrs) { super(context, attrs); - - final Resources res = getResources(); - - // Measure for initial configuration - mMaxItems = getMaxActionButtons(); - - // TODO There has to be a better way to indicate that we don't have a hard menu key. - final Configuration config = res.getConfiguration(); - mReserveOverflow = config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); - mWidthLimit = res.getDisplayMetrics().widthPixels / 2; - - TypedArray a = context.obtainStyledAttributes(com.android.internal.R.styleable.Theme); - mDivider = a.getDrawable(com.android.internal.R.styleable.Theme_dividerVertical); - a.recycle(); - - mDividerPadding = DIVIDER_PADDING * res.getDisplayMetrics().density; - setBaselineAligned(false); } + public void setPresenter(ActionMenuPresenter presenter) { + mPresenter = presenter; + } + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - mReserveOverflow = newConfig.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); - mMaxItems = getMaxActionButtons(); - mWidthLimit = getResources().getDisplayMetrics().widthPixels / 2; - if (mMenu != null) { - mMenu.setMaxActionItems(mMaxItems); - updateChildren(false); + mPresenter.updateMenuView(false); + + if (mPresenter != null && mPresenter.isOverflowMenuShowing()) { + mPresenter.hideOverflowMenu(); + mPresenter.showOverflowMenu(); } + } - if (mOverflowPopup != null && mOverflowPopup.isShowing()) { - mOverflowPopup.dismiss(); - post(mShowOverflow); + @Override + public void requestLayout() { + // Layout can influence how many action items fit. + mUpdateContentsBeforeMeasure = true; + super.requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mUpdateContentsBeforeMeasure && mMenu != null) { + mMenu.onItemsChanged(true); + mUpdateContentsBeforeMeasure = false; } + // If we've been given an exact size to match, apply special formatting during layout. + mFormatItems = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - if (mOverflowPopup != null && mOverflowPopup.isShowing()) { - mOverflowPopup.dismiss(); + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (!mFormatItems) { + super.onLayout(changed, left, top, right, bottom); + return; + } + + final int childCount = getChildCount(); + final int midVertical = (top + bottom) / 2; + final int dividerWidth = getDividerWidth(); + boolean hasOverflow = false; + int overflowWidth = 0; + int nonOverflowWidth = 0; + int nonOverflowCount = 0; + int widthRemaining = right - left - getPaddingRight() - getPaddingLeft(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + if (v.getVisibility() == GONE) { + continue; + } + + LayoutParams p = (LayoutParams) v.getLayoutParams(); + if (p.isOverflowButton) { + hasOverflow = true; + overflowWidth = v.getMeasuredWidth(); + if (hasDividerBeforeChildAt(i)) { + overflowWidth += dividerWidth; + } + + int height = v.getMeasuredHeight(); + int r = getPaddingRight(); + int l = r - overflowWidth; + int t = midVertical - (height / 2); + int b = t + height; + v.layout(l, t, r, b); + + widthRemaining -= overflowWidth; + } else { + nonOverflowWidth += v.getMeasuredWidth() + p.leftMargin + p.rightMargin; + if (hasDividerBeforeChildAt(i)) { + nonOverflowWidth += dividerWidth; + } + nonOverflowCount++; + } } - removeCallbacks(mShowOverflow); - if (mPostedOpenRunnable != null) { - removeCallbacks(mPostedOpenRunnable); + + // Try to center non-overflow items with uniformly spaced padding, including on the edges. + // Overflow will always pin to the right edge. If there isn't enough room for that, + // center in the remaining space. + if (nonOverflowWidth <= widthRemaining - overflowWidth) { + widthRemaining -= overflowWidth; + } + + final int spacing = (widthRemaining - nonOverflowWidth) / (nonOverflowCount + 1); + int startLeft = getPaddingLeft() + overflowWidth + spacing; + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + final LayoutParams lp = (LayoutParams) v.getLayoutParams(); + if (v.getVisibility() == GONE || lp.isOverflowButton) { + continue; + } + + startLeft += lp.leftMargin; + int width = v.getMeasuredWidth(); + int height = v.getMeasuredHeight(); + int t = midVertical - (height / 2); + v.layout(startLeft, t, startLeft + width, t + height); + startLeft += width + lp.rightMargin + spacing; } } - private int getMaxActionButtons() { - return getResources().getInteger(com.android.internal.R.integer.max_action_buttons); + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mPresenter.dismissPopupMenus(); } public boolean isOverflowReserved() { @@ -141,10 +163,6 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo public void setOverflowReserved(boolean reserveOverflow) { mReserveOverflow = reserveOverflow; } - - public View getOverflowButton() { - return mOverflowButton; - } @Override protected LayoutParams generateDefaultLayoutParams() { @@ -166,6 +184,17 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo return generateDefaultLayoutParams(); } + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + + public LayoutParams generateOverflowButtonLayoutParams() { + LayoutParams result = generateDefaultLayoutParams(); + result.isOverflowButton = true; + return result; + } + public boolean invokeItem(MenuItemImpl item) { return mMenu.performItemAction(item, 0); } @@ -174,243 +203,50 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo return 0; } - public void initialize(MenuBuilder menu, int menuType) { - int width = mWidthLimit; - if (mReserveOverflow) { - if (mOverflowButton == null) { - OverflowMenuButton button = new OverflowMenuButton(mContext); - mOverflowButton = button; - } - final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - mOverflowButton.measure(spec, spec); - width -= mOverflowButton.getMeasuredWidth(); - } - - menu.setActionWidthLimit(width); - - menu.setMaxActionItems(mMaxItems); - final boolean cleared = mMenu != menu; + public void initialize(MenuBuilder menu) { mMenu = menu; - updateChildren(cleared); } - public void updateChildren(boolean cleared) { - final ArrayList<MenuItemImpl> itemsToShow = mMenu.getActionItems(mReserveOverflow); - final int itemCount = itemsToShow.size(); - - boolean needsDivider = false; - int childIndex = 0; - for (int i = 0; i < itemCount; i++) { - final MenuItemImpl itemData = itemsToShow.get(i); - boolean hasDivider = false; - - if (needsDivider) { - if (!isDivider(getChildAt(childIndex))) { - addView(makeDividerView(), childIndex, makeDividerLayoutParams()); - } - hasDivider = true; - childIndex++; - } - - View childToAdd = itemData.getActionView(); - boolean needsPreDivider = false; - if (childToAdd != null) { - childToAdd.setLayoutParams(makeActionViewLayoutParams(childToAdd)); - } else { - ActionMenuItemView view = (ActionMenuItemView) itemData.getItemView( - MenuBuilder.TYPE_ACTION_BUTTON, this); - view.setItemInvoker(this); - needsPreDivider = i > 0 && !hasDivider && view.hasText() && - itemData.getIcon() == null; - needsDivider = view.hasText(); - childToAdd = view; - } - - boolean addPreDivider = removeChildrenUntil(childIndex, childToAdd, needsPreDivider); - - if (addPreDivider) addView(makeDividerView(), childIndex, makeDividerLayoutParams()); - if (needsPreDivider) childIndex++; - - if (getChildAt(childIndex) != childToAdd) { - addView(childToAdd, childIndex); - } - childIndex++; - } - - final boolean hasOverflow = mOverflowButton != null && mOverflowButton.getParent() == this; - final boolean needsOverflow = mReserveOverflow && mMenu.getNonActionItems(true).size() > 0; - - if (hasOverflow != needsOverflow) { - if (needsOverflow) { - if (mOverflowButton == null) { - OverflowMenuButton button = new OverflowMenuButton(mContext); - mOverflowButton = button; - } - boolean addDivider = removeChildrenUntil(childIndex, mOverflowButton, true); - if (addDivider && itemCount > 0) { - addView(makeDividerView(), childIndex, makeDividerLayoutParams()); - childIndex++; - } - addView(mOverflowButton, childIndex); - childIndex++; - } else { - removeView(mOverflowButton); - } - } else { - if (needsOverflow) { - boolean overflowDivider = itemCount > 0; - boolean addDivider = removeChildrenUntil(childIndex, mOverflowButton, - overflowDivider); - if (addDivider && itemCount > 0) { - addView(makeDividerView(), childIndex, makeDividerLayoutParams()); - } - if (overflowDivider) { - childIndex += 2; - } else { - childIndex++; - } - } - } - - while (getChildCount() > childIndex) { - removeViewAt(childIndex); - } - } - - private boolean removeChildrenUntil(int start, View targetChild, boolean needsPreDivider) { - final int childCount = getChildCount(); - boolean found = false; - for (int i = start; i < childCount; i++) { - final View child = getChildAt(i); - if (child == targetChild) { - found = true; - break; - } - } - - if (!found) { - return needsPreDivider; - } - - for (int i = start; i < getChildCount(); ) { - final View child = getChildAt(i); - if (needsPreDivider && isDivider(child)) { - needsPreDivider = false; - i++; - continue; - } - if (child == targetChild) break; - removeViewAt(i); - } - - return needsPreDivider; - } - - private static boolean isDivider(View v) { - return v != null && v.getId() == com.android.internal.R.id.action_menu_divider; - } - - public boolean showOverflowMenu() { - if (mOverflowButton != null && !isOverflowMenuShowing()) { - mMenu.getCallback().onMenuModeChange(mMenu); - return true; - } - return false; - } - - public void openOverflowMenu() { - OverflowPopup popup = new OverflowPopup(getContext(), mMenu, mOverflowButton, true); - mPostedOpenRunnable = new OpenOverflowRunnable(popup); - // Post this for later; we might still need a layout for the anchor to be right. - post(mPostedOpenRunnable); - } - - public boolean isOverflowMenuShowing() { - return mOverflowPopup != null && mOverflowPopup.isShowing(); - } - - public boolean isOverflowMenuOpen() { - return mOverflowPopup != null; - } - - public boolean hideOverflowMenu() { - if (mPostedOpenRunnable != null) { - removeCallbacks(mPostedOpenRunnable); - return true; + @Override + protected boolean hasDividerBeforeChildAt(int childIndex) { + final View childBefore = getChildAt(childIndex - 1); + final View child = getChildAt(childIndex); + boolean result = false; + if (childIndex < getChildCount() && childBefore instanceof ActionMenuChildView) { + result |= ((ActionMenuChildView) childBefore).needsDividerAfter(); } - - MenuPopupHelper popup = mOverflowPopup; - if (popup != null) { - popup.dismiss(); - return true; + if (childIndex > 0 && child instanceof ActionMenuChildView) { + result |= ((ActionMenuChildView) child).needsDividerBefore(); } - return false; - } - - private boolean addItemView(boolean needsDivider, ActionMenuItemView view) { - view.setItemInvoker(this); - boolean hasText = view.hasText(); - - if (hasText && needsDivider) { - addView(makeDividerView(), makeDividerLayoutParams()); - } - addView(view); - return hasText; - } - - private ImageView makeDividerView() { - ImageView result = new ImageView(mContext); - result.setImageDrawable(mDivider); - result.setScaleType(ImageView.ScaleType.FIT_XY); - result.setId(com.android.internal.R.id.action_menu_divider); return result; } - private LayoutParams makeDividerLayoutParams() { - LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.MATCH_PARENT); - params.topMargin = (int) mDividerPadding; - params.bottomMargin = (int) mDividerPadding; - return params; - } - - private LayoutParams makeActionViewLayoutParams(View view) { - return generateLayoutParams(view.getLayoutParams()); + public interface ActionMenuChildView { + public boolean needsDividerBefore(); + public boolean needsDividerAfter(); } - private class OverflowMenuButton extends ImageButton { - public OverflowMenuButton(Context context) { - super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle); + public static class LayoutParams extends LinearLayout.LayoutParams { + @ViewDebug.ExportedProperty(category = "layout") + public boolean isOverflowButton; - setClickable(true); - setFocusable(true); - setVisibility(VISIBLE); - setEnabled(true); + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); } - @Override - public boolean performClick() { - if (super.performClick()) { - return true; - } - - playSoundEffect(SoundEffectConstants.CLICK); - showOverflowMenu(); - return true; + public LayoutParams(LayoutParams other) { + super((LinearLayout.LayoutParams) other); + isOverflowButton = other.isOverflowButton; } - } - private class OverflowPopup extends MenuPopupHelper { - public OverflowPopup(Context context, MenuBuilder menu, View anchorView, - boolean overflowOnly) { - super(context, menu, anchorView, overflowOnly); + public LayoutParams(int width, int height) { + super(width, height); + isOverflowButton = false; } - @Override - public void onDismiss() { - super.onDismiss(); - mMenu.getCallback().onCloseMenu(mMenu, true); - mOverflowPopup = null; + public LayoutParams(int width, int height, boolean isOverflowButton) { + super(width, height); + this.isOverflowButton = isOverflowButton; } } } diff --git a/core/java/com/android/internal/view/menu/BaseMenuPresenter.java b/core/java/com/android/internal/view/menu/BaseMenuPresenter.java new file mode 100644 index 0000000..16f51fd --- /dev/null +++ b/core/java/com/android/internal/view/menu/BaseMenuPresenter.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view.menu; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; + +/** + * Base class for MenuPresenters that have a consistent container view and item + * views. Behaves similarly to an AdapterView in that existing item views will + * be reused if possible when items change. + */ +public abstract class BaseMenuPresenter implements MenuPresenter { + protected Context mContext; + protected MenuBuilder mMenu; + protected LayoutInflater mInflater; + private Callback mCallback; + + private int mMenuLayoutRes; + private int mItemLayoutRes; + + protected MenuView mMenuView; + + /** + * Construct a new BaseMenuPresenter. + * + * @param menuLayoutRes Layout resource ID for the menu container view + * @param itemLayoutRes Layout resource ID for a single item view + */ + public BaseMenuPresenter(int menuLayoutRes, int itemLayoutRes) { + mMenuLayoutRes = menuLayoutRes; + mItemLayoutRes = itemLayoutRes; + } + + @Override + public void initForMenu(Context context, MenuBuilder menu) { + mContext = context; + mInflater = LayoutInflater.from(mContext); + mMenu = menu; + } + + @Override + public MenuView getMenuView(ViewGroup root) { + if (mMenuView == null) { + mMenuView = (MenuView) mInflater.inflate(mMenuLayoutRes, root, false); + mMenuView.initialize(mMenu); + updateMenuView(true); + } + + return mMenuView; + } + + /** + * Reuses item views when it can + */ + public void updateMenuView(boolean cleared) { + mMenu.flagActionItems(); + ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems(); + final int itemCount = visibleItems.size(); + final ViewGroup parent = (ViewGroup) mMenuView; + int childIndex = 0; + for (int i = 0; i < itemCount; i++) { + MenuItemImpl item = visibleItems.get(i); + if (shouldIncludeItem(childIndex, item)) { + final View convertView = parent.getChildAt(childIndex); + final View itemView = getItemView(item, convertView, parent); + if (itemView != convertView) { + addItemView(itemView, childIndex); + } + childIndex++; + } + } + + // Remove leftover views. + while (childIndex < parent.getChildCount()) { + if (!filterLeftoverView(parent, childIndex)) { + childIndex++; + } + } + } + + /** + * Add an item view at the given index. + * + * @param itemView View to add + * @param childIndex Index within the parent to insert at + */ + protected void addItemView(View itemView, int childIndex) { + final ViewGroup currentParent = (ViewGroup) itemView.getParent(); + if (currentParent != null) { + currentParent.removeView(itemView); + } + ((ViewGroup) mMenuView).addView(itemView, childIndex); + } + + /** + * Filter the child view at index and remove it if appropriate. + * @param parent Parent to filter from + * @param childIndex Index to filter + * @return true if the child view at index was removed + */ + protected boolean filterLeftoverView(ViewGroup parent, int childIndex) { + parent.removeViewAt(childIndex); + return true; + } + + public void setCallback(Callback cb) { + mCallback = cb; + } + + /** + * Create a new item view that can be re-bound to other item data later. + * + * @return The new item view + */ + public MenuView.ItemView createItemView(ViewGroup parent) { + return (MenuView.ItemView) mInflater.inflate(mItemLayoutRes, parent, false); + } + + /** + * Prepare an item view for use. See AdapterView for the basic idea at work here. + * This may require creating a new item view, but well-behaved implementations will + * re-use the view passed as convertView if present. The returned view will be populated + * with data from the item parameter. + * + * @param item Item to present + * @param convertView Existing view to reuse + * @param parent Intended parent view - use for inflation. + * @return View that presents the requested menu item + */ + public View getItemView(MenuItemImpl item, View convertView, ViewGroup parent) { + MenuView.ItemView itemView; + if (convertView instanceof MenuView.ItemView) { + itemView = (MenuView.ItemView) convertView; + } else { + itemView = createItemView(parent); + } + bindItemView(item, itemView); + return (View) itemView; + } + + /** + * Bind item data to an existing item view. + * + * @param item Item to bind + * @param itemView View to populate with item data + */ + public abstract void bindItemView(MenuItemImpl item, MenuView.ItemView itemView); + + /** + * Filter item by child index and item data. + * + * @param childIndex Indended presentation index of this item + * @param item Item to present + * @return true if this item should be included in this menu presentation; false otherwise + */ + public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) { + return true; + } + + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + if (mCallback != null) { + mCallback.onCloseMenu(menu, allMenusAreClosing); + } + } + + public boolean onSubMenuSelected(SubMenuBuilder menu) { + if (mCallback != null) { + return mCallback.onOpenSubMenu(menu); + } + return false; + } + + public boolean flagActionItems() { + return false; + } +} diff --git a/core/java/com/android/internal/view/menu/ExpandedMenuView.java b/core/java/com/android/internal/view/menu/ExpandedMenuView.java index 9e4b4ce..723ece4 100644 --- a/core/java/com/android/internal/view/menu/ExpandedMenuView.java +++ b/core/java/com/android/internal/view/menu/ExpandedMenuView.java @@ -17,17 +17,15 @@ package com.android.internal.view.menu; +import com.android.internal.view.menu.MenuBuilder.ItemInvoker; + import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.widget.AdapterView; -import android.widget.BaseAdapter; -import android.widget.ListAdapter; -import android.widget.ListView; import android.widget.AdapterView.OnItemClickListener; - -import com.android.internal.view.menu.MenuBuilder.ItemInvoker; +import android.widget.ListView; /** * The expanded menu view is a list-like menu with all of the available menu items. It is opened @@ -53,23 +51,8 @@ public final class ExpandedMenuView extends ListView implements ItemInvoker, Men setOnItemClickListener(this); } - public void initialize(MenuBuilder menu, int menuType) { + public void initialize(MenuBuilder menu) { mMenu = menu; - - setAdapter(menu.new MenuAdapter(menuType)); - } - - public void updateChildren(boolean cleared) { - ListAdapter adapter = getAdapter(); - // Tell adapter of the change, it will notify the mListView - if (adapter != null) { - if (cleared) { - ((BaseAdapter)adapter).notifyDataSetInvalidated(); - } - else { - ((BaseAdapter)adapter).notifyDataSetChanged(); - } - } } @Override diff --git a/core/java/com/android/internal/view/menu/IconMenuItemView.java b/core/java/com/android/internal/view/menu/IconMenuItemView.java index 3c5b422..afa8a01 100644 --- a/core/java/com/android/internal/view/menu/IconMenuItemView.java +++ b/core/java/com/android/internal/view/menu/IconMenuItemView.java @@ -112,6 +112,10 @@ public final class IconMenuItemView extends TextView implements MenuView.ItemVie setEnabled(itemData.isEnabled()); } + public void setItemData(MenuItemImpl data) { + mItemData = data; + } + @Override public boolean performClick() { // Let the view's click listener have top priority (the More button relies on this) diff --git a/core/java/com/android/internal/view/menu/IconMenuPresenter.java b/core/java/com/android/internal/view/menu/IconMenuPresenter.java new file mode 100644 index 0000000..f717904 --- /dev/null +++ b/core/java/com/android/internal/view/menu/IconMenuPresenter.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.view.menu; + +import com.android.internal.view.menu.MenuView.ItemView; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.SparseArray; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; + +/** + * MenuPresenter for the classic "six-pack" icon menu. + */ +public class IconMenuPresenter extends BaseMenuPresenter { + private IconMenuItemView mMoreView; + private int mMaxItems = -1; + + private static final String VIEWS_TAG = "android:menu:icon"; + + public IconMenuPresenter() { + super(com.android.internal.R.layout.icon_menu_layout, + com.android.internal.R.layout.icon_menu_item_layout); + } + + @Override + public void initForMenu(Context context, MenuBuilder menu) { + mContext = new ContextThemeWrapper(context, com.android.internal.R.style.Theme_IconMenu); + mInflater = LayoutInflater.from(mContext); + mMenu = menu; + mMaxItems = -1; + } + + @Override + public void bindItemView(MenuItemImpl item, ItemView itemView) { + final IconMenuItemView view = (IconMenuItemView) itemView; + view.setItemData(item); + + view.initialize(item.getTitleForItemView(view), item.getIcon()); + + view.setVisibility(item.isVisible() ? View.VISIBLE : View.GONE); + view.setEnabled(view.isEnabled()); + view.setLayoutParams(view.getTextAppropriateLayoutParams()); + } + + @Override + public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) { + final ArrayList<MenuItemImpl> itemsToShow = mMenu.getNonActionItems(); + boolean fits = (itemsToShow.size() == mMaxItems && childIndex < mMaxItems) || + childIndex < mMaxItems - 1; + return fits && !item.isActionButton(); + } + + @Override + protected void addItemView(View itemView, int childIndex) { + final IconMenuItemView v = (IconMenuItemView) itemView; + final IconMenuView parent = (IconMenuView) mMenuView; + + v.setIconMenuView(parent); + v.setItemInvoker(parent); + v.setBackgroundDrawable(parent.getItemBackgroundDrawable()); + super.addItemView(itemView, childIndex); + } + + @Override + public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + if (!subMenu.hasVisibleItems()) return false; + + // The window manager will give us a token. + new MenuDialogHelper(subMenu).show(null); + super.onSubMenuSelected(subMenu); + return true; + } + + @Override + public void updateMenuView(boolean cleared) { + final IconMenuView menuView = (IconMenuView) mMenuView; + if (mMaxItems < 0) mMaxItems = menuView.getMaxItems(); + final ArrayList<MenuItemImpl> itemsToShow = mMenu.getNonActionItems(); + final boolean needsMore = itemsToShow.size() > mMaxItems; + super.updateMenuView(cleared); + + if (needsMore && (mMoreView == null || mMoreView.getParent() != menuView)) { + if (mMoreView == null) { + mMoreView = menuView.createMoreItemView(); + mMoreView.setBackgroundDrawable(menuView.getItemBackgroundDrawable()); + } + menuView.addView(mMoreView); + } else if (!needsMore && mMoreView != null) { + menuView.removeView(mMoreView); + } + + menuView.setNumActualItemsShown(needsMore ? mMaxItems - 1 : itemsToShow.size()); + } + + @Override + protected boolean filterLeftoverView(ViewGroup parent, int childIndex) { + if (parent.getChildAt(childIndex) != mMoreView) { + return super.filterLeftoverView(parent, childIndex); + } + return false; + } + + public int getNumActualItemsShown() { + return ((IconMenuView) mMenuView).getNumActualItemsShown(); + } + + public void saveHierarchyState(Bundle outState) { + SparseArray<Parcelable> viewStates = new SparseArray<Parcelable>(); + if (mMenuView != null) { + ((View) mMenuView).saveHierarchyState(viewStates); + } + outState.putSparseParcelableArray(VIEWS_TAG, viewStates); + } + + public void restoreHierarchyState(Bundle inState) { + SparseArray<Parcelable> viewStates = inState.getSparseParcelableArray(VIEWS_TAG); + if (viewStates != null) { + ((View) mMenuView).restoreHierarchyState(viewStates); + } + } +} diff --git a/core/java/com/android/internal/view/menu/IconMenuView.java b/core/java/com/android/internal/view/menu/IconMenuView.java index d18c9727..dab43eb 100644 --- a/core/java/com/android/internal/view/menu/IconMenuView.java +++ b/core/java/com/android/internal/view/menu/IconMenuView.java @@ -80,10 +80,7 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi /** Icon for the 'More' button */ private Drawable mMoreIcon; - - /** Item view for the 'More' button */ - private IconMenuItemView mMoreItemView; - + /** Background of each item (should contain the selected and focused states) */ private Drawable mItemBackground; @@ -172,6 +169,10 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); } + int getMaxItems() { + return mMaxItems; + } + /** * Figures out the layout for the menu items. * @@ -277,23 +278,8 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi return true; } - /** - * Adds an IconMenuItemView to this icon menu view. - * @param itemView The item's view to add - */ - private void addItemView(IconMenuItemView itemView) { - // Set ourselves on the item view - itemView.setIconMenuView(this); - - // Apply the background to the item view - itemView.setBackgroundDrawable( - mItemBackground.getConstantState().newDrawable( - getContext().getResources())); - - // This class is the invoker for all its item views - itemView.setItemInvoker(this); - - addView(itemView, itemView.getTextAppropriateLayoutParams()); + Drawable getItemBackgroundDrawable() { + return mItemBackground.getConstantState().newDrawable(getContext().getResources()); } /** @@ -302,25 +288,23 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi * have a MenuItemData backing it. * @return The IconMenuItemView for the 'More' button */ - private IconMenuItemView createMoreItemView() { - LayoutInflater inflater = mMenu.getMenuType(MenuBuilder.TYPE_ICON).getInflater(); + IconMenuItemView createMoreItemView() { + Context context = getContext(); + LayoutInflater inflater = LayoutInflater.from(context); final IconMenuItemView itemView = (IconMenuItemView) inflater.inflate( com.android.internal.R.layout.icon_menu_item_layout, null); - Resources r = getContext().getResources(); + Resources r = context.getResources(); itemView.initialize(r.getText(com.android.internal.R.string.more_item_label), mMoreIcon); // Set up a click listener on the view since there will be no invocation sequence // due to the lack of a MenuItemData this view itemView.setOnClickListener(new OnClickListener() { public void onClick(View v) { - // Switches the menu to expanded mode - MenuBuilder.Callback cb = mMenu.getCallback(); - if (cb != null) { - // Call callback - cb.onMenuModeChange(mMenu); - } + // Switches the menu to expanded mode. Requires support from + // the menu's active callback. + mMenu.changeMenuMode(); } }); @@ -328,51 +312,8 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi } - public void initialize(MenuBuilder menu, int menuType) { + public void initialize(MenuBuilder menu) { mMenu = menu; - updateChildren(true); - } - - public void updateChildren(boolean cleared) { - // This method does a clear refresh of children - removeAllViews(); - - // IconMenuView never wants content sorted for an overflow action button, since - // it is never used in the presence of an overflow button. - final ArrayList<MenuItemImpl> itemsToShow = mMenu.getNonActionItems(false); - final int numItems = itemsToShow.size(); - final int numItemsThatCanFit = mMaxItems; - // Minimum of the num that can fit and the num that we have - final int minFitMinus1AndNumItems = Math.min(numItemsThatCanFit - 1, numItems); - - MenuItemImpl itemData; - // Traverse through all but the last item that can fit since that last item can either - // be a 'More' button or a sixth item - for (int i = 0; i < minFitMinus1AndNumItems; i++) { - itemData = itemsToShow.get(i); - addItemView((IconMenuItemView) itemData.getItemView(MenuBuilder.TYPE_ICON, this)); - } - - if (numItems > numItemsThatCanFit) { - // If there are more items than we can fit, show the 'More' button to - // switch to expanded mode - if (mMoreItemView == null) { - mMoreItemView = createMoreItemView(); - } - - addItemView(mMoreItemView); - - // The last view is the more button, so the actual number of items is one less than - // the number that can fit - mNumActualItemsShown = numItemsThatCanFit - 1; - } else if (numItems == numItemsThatCanFit) { - // There are exactly the number we can show, so show the last item - final MenuItemImpl lastItemData = itemsToShow.get(numItemsThatCanFit - 1); - addItemView((IconMenuItemView) lastItemData.getItemView(MenuBuilder.TYPE_ICON, this)); - - // The items shown fit exactly - mNumActualItemsShown = numItemsThatCanFit; - } } /** @@ -463,13 +404,6 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (mHasStaleChildren) { - mHasStaleChildren = false; - - // If we have stale data, resync with the menu - updateChildren(false); - } - int measuredWidth = resolveSize(Integer.MAX_VALUE, widthMeasureSpec); calculateItemFittingMetadata(measuredWidth); layoutItems(measuredWidth); @@ -564,6 +498,9 @@ public final class IconMenuView extends ViewGroup implements ItemInvoker, MenuVi return mNumActualItemsShown; } + void setNumActualItemsShown(int count) { + mNumActualItemsShown = count; + } public int getWindowAnimations() { return mAnimations; diff --git a/core/java/com/android/internal/view/menu/ListMenuItemView.java b/core/java/com/android/internal/view/menu/ListMenuItemView.java index 02584b6..0c3c605 100644 --- a/core/java/com/android/internal/view/menu/ListMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ListMenuItemView.java @@ -48,6 +48,8 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView private int mMenuType; + private LayoutInflater mInflater; + public ListMenuItemView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs); @@ -187,7 +189,7 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView } public void setIcon(Drawable icon) { - final boolean showIcon = mItemData.shouldShowIcon(mMenuType); + final boolean showIcon = mItemData.shouldShowIcon(); if (!showIcon && !mPreserveIconSpacing) { return; } @@ -212,14 +214,14 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView } private void insertIconView() { - LayoutInflater inflater = mItemData.getLayoutInflater(mMenuType); + LayoutInflater inflater = getInflater(); mIconView = (ImageView) inflater.inflate(com.android.internal.R.layout.list_menu_item_icon, this, false); addView(mIconView, 0); } private void insertRadioButton() { - LayoutInflater inflater = mItemData.getLayoutInflater(mMenuType); + LayoutInflater inflater = getInflater(); mRadioButton = (RadioButton) inflater.inflate(com.android.internal.R.layout.list_menu_item_radio, this, false); @@ -227,7 +229,7 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView } private void insertCheckBox() { - LayoutInflater inflater = mItemData.getLayoutInflater(mMenuType); + LayoutInflater inflater = getInflater(); mCheckBox = (CheckBox) inflater.inflate(com.android.internal.R.layout.list_menu_item_checkbox, this, false); @@ -242,4 +244,10 @@ public class ListMenuItemView extends LinearLayout implements MenuView.ItemView return false; } + private LayoutInflater getInflater() { + if (mInflater == null) { + mInflater = LayoutInflater.from(mContext); + } + return mInflater; + } } diff --git a/core/java/com/android/internal/view/menu/ListMenuPresenter.java b/core/java/com/android/internal/view/menu/ListMenuPresenter.java new file mode 100644 index 0000000..2cb2a10 --- /dev/null +++ b/core/java/com/android/internal/view/menu/ListMenuPresenter.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view.menu; + +import android.content.Context; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.SparseArray; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ListAdapter; + +import java.util.ArrayList; + +/** + * MenuPresenter for list-style menus. + */ +public class ListMenuPresenter implements MenuPresenter, AdapterView.OnItemClickListener { + Context mContext; + LayoutInflater mInflater; + MenuBuilder mMenu; + + ExpandedMenuView mMenuView; + + private int mItemIndexOffset; + int mThemeRes; + int mItemLayoutRes; + + private Callback mCallback; + private MenuAdapter mAdapter; + + public static final String VIEWS_TAG = "android:menu:list"; + + /** + * Construct a new ListMenuPresenter. + * @param context Context to use for theming. This will supersede the context provided + * to initForMenu when this presenter is added. + * @param itemLayoutRes Layout resource for individual item views. + */ + public ListMenuPresenter(Context context, int itemLayoutRes) { + this(itemLayoutRes, 0); + mContext = context; + } + + /** + * Construct a new ListMenuPresenter. + * @param itemLayoutRes Layout resource for individual item views. + * @param themeRes Resource ID of a theme to use for views. + */ + public ListMenuPresenter(int itemLayoutRes, int themeRes) { + mItemLayoutRes = itemLayoutRes; + mThemeRes = themeRes; + } + + @Override + public void initForMenu(Context context, MenuBuilder menu) { + if (mThemeRes != 0) { + mContext = new ContextThemeWrapper(context, mThemeRes); + } else if (mContext == null) { + mContext = context; + } + mInflater = LayoutInflater.from(mContext); + mMenu = menu; + } + + @Override + public MenuView getMenuView(ViewGroup root) { + if (mMenuView == null) { + mMenuView = (ExpandedMenuView) mInflater.inflate( + com.android.internal.R.layout.expanded_menu_layout, root, false); + if (mAdapter == null) { + mAdapter = new MenuAdapter(); + } + mMenuView.setAdapter(mAdapter); + mMenuView.setOnItemClickListener(this); + } + return mMenuView; + } + + /** + * Call this instead of getMenuView if you want to manage your own ListView. + * For proper operation, the ListView hosting this adapter should add + * this presenter as an OnItemClickListener. + * + * @return A ListAdapter containing the items in the menu. + */ + public ListAdapter getAdapter() { + if (mAdapter == null) { + mAdapter = new MenuAdapter(); + } + return mAdapter; + } + + @Override + public void updateMenuView(boolean cleared) { + if (mAdapter != null) mAdapter.notifyDataSetChanged(); + } + + @Override + public void setCallback(Callback cb) { + mCallback = cb; + } + + @Override + public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + if (!subMenu.hasVisibleItems()) return false; + + // The window manager will give us a token. + new MenuDialogHelper(subMenu).show(null); + if (mCallback != null) { + mCallback.onOpenSubMenu(subMenu); + } + return true; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + if (mCallback != null) { + mCallback.onCloseMenu(menu, allMenusAreClosing); + } + } + + int getItemIndexOffset() { + return mItemIndexOffset; + } + + public void setItemIndexOffset(int offset) { + mItemIndexOffset = offset; + if (mMenuView != null) { + updateMenuView(false); + } + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + mMenu.performItemAction(mAdapter.getItem(position), 0); + } + + @Override + public boolean flagActionItems() { + return false; + } + + public void saveHierarchyState(Bundle outState) { + SparseArray<Parcelable> viewStates = new SparseArray<Parcelable>(); + if (mMenuView != null) { + ((View) mMenuView).saveHierarchyState(viewStates); + } + outState.putSparseParcelableArray(VIEWS_TAG, viewStates); + } + + public void restoreHierarchyState(Bundle inState) { + SparseArray<Parcelable> viewStates = inState.getSparseParcelableArray(VIEWS_TAG); + ((View) mMenuView).restoreHierarchyState(viewStates); + } + + private class MenuAdapter extends BaseAdapter { + public int getCount() { + ArrayList<MenuItemImpl> items = mMenu.getVisibleItems(); + return items.size() - mItemIndexOffset; + } + + public MenuItemImpl getItem(int position) { + ArrayList<MenuItemImpl> items = mMenu.getVisibleItems(); + return items.get(position + mItemIndexOffset); + } + + public long getItemId(int position) { + // Since a menu item's ID is optional, we'll use the position as an + // ID for the item in the AdapterView + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(mItemLayoutRes, parent, false); + } + + MenuView.ItemView itemView = (MenuView.ItemView) convertView; + itemView.initialize(getItem(position), 0); + return convertView; + } + } +} diff --git a/core/java/com/android/internal/view/menu/MenuBuilder.java b/core/java/com/android/internal/view/menu/MenuBuilder.java index 14d0ac5..e9fcb23 100644 --- a/core/java/com/android/internal/view/menu/MenuBuilder.java +++ b/core/java/com/android/internal/view/menu/MenuBuilder.java @@ -25,29 +25,22 @@ import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.drawable.Drawable; -import android.os.Bundle; import android.os.Parcelable; +import android.util.Log; import android.util.SparseArray; -import android.util.SparseBooleanArray; -import android.util.TypedValue; import android.view.ContextMenu.ContextMenuInfo; -import android.view.ContextThemeWrapper; import android.view.KeyCharacterMap; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; -import android.view.View.MeasureSpec; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.BaseAdapter; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; -import java.util.Vector; +import java.util.concurrent.CopyOnWriteArrayList; /** * Implementation of the {@link android.view.Menu} interface for creating a @@ -55,60 +48,6 @@ import java.util.Vector; */ public class MenuBuilder implements Menu { private static final String LOGTAG = "MenuBuilder"; - - /** The number of different menu types */ - public static final int NUM_TYPES = 5; - /** The menu type that represents the icon menu view */ - public static final int TYPE_ICON = 0; - /** The menu type that represents the expanded menu view */ - public static final int TYPE_EXPANDED = 1; - /** - * The menu type that represents a menu dialog. Examples are context and sub - * menus. This menu type will not have a corresponding MenuView, but it will - * have an ItemView. - */ - public static final int TYPE_DIALOG = 2; - /** - * The menu type that represents a button in the application's action bar. - */ - public static final int TYPE_ACTION_BUTTON = 3; - /** - * The menu type that represents a menu popup. - */ - public static final int TYPE_POPUP = 4; - - private static final String VIEWS_TAG = "android:views"; - - private static final int THEME_SYSTEM_DEFAULT = 0; - private static final int THEME_APPLICATION = -1; - private static final int THEME_ALERT_DIALOG = -2; - - // Order must be the same order as the TYPE_* - static final int THEME_RES_FOR_TYPE[] = new int[] { - com.android.internal.R.style.Theme_IconMenu, - com.android.internal.R.style.Theme_ExpandedMenu, - THEME_ALERT_DIALOG, - THEME_APPLICATION, - THEME_APPLICATION, - }; - - // Order must be the same order as the TYPE_* - static final int LAYOUT_RES_FOR_TYPE[] = new int[] { - com.android.internal.R.layout.icon_menu_layout, - com.android.internal.R.layout.expanded_menu_layout, - 0, - com.android.internal.R.layout.action_menu_layout, - 0, - }; - - // Order must be the same order as the TYPE_* - static final int ITEM_LAYOUT_RES_FOR_TYPE[] = new int[] { - com.android.internal.R.layout.icon_menu_item_layout, - com.android.internal.R.layout.list_menu_item_layout, - com.android.internal.R.layout.list_menu_item_layout, - com.android.internal.R.layout.action_menu_item_layout, - com.android.internal.R.layout.popup_menu_item_layout, - }; private static final int[] sCategoryToOrder = new int[] { 1, /* No category */ @@ -160,14 +99,7 @@ public class MenuBuilder implements Menu { * Contains items that should NOT appear in the Action Bar, if present. */ private ArrayList<MenuItemImpl> mNonActionItems; - /** - * The number of visible action buttons permitted in this menu - */ - private int mMaxActionItems; - /** - * The total width limit in pixels for all action items within a menu - */ - private int mActionWidthLimit; + /** * Whether or not the items (or any one item's action state) has changed since it was * last fetched. @@ -175,12 +107,6 @@ public class MenuBuilder implements Menu { private boolean mIsActionItemsStale; /** - * Whether the process of granting space as action items should reserve a space for - * an overflow option in the action list. - */ - private boolean mReserveActionOverflow; - - /** * Default value for how added items should show in the action list. */ private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER; @@ -210,100 +136,19 @@ public class MenuBuilder implements Menu { * that may individually call onItemsChanged. */ private boolean mPreventDispatchingItemsChanged = false; + private boolean mItemsChangedWhileDispatchPrevented = false; private boolean mOptionalIconsVisible = false; - private ViewGroup mMeasureActionButtonParent; - - private final WeakReference<MenuAdapter>[] mAdapterCache = - new WeakReference[NUM_TYPES]; - private final WeakReference<OverflowMenuAdapter>[] mOverflowAdapterCache = - new WeakReference[NUM_TYPES]; - - // Group IDs that have been added as actions - used temporarily, allocated here for reuse. - private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray(); - - private static int getAlertDialogTheme(Context context) { - TypedValue outValue = new TypedValue(); - context.getTheme().resolveAttribute(com.android.internal.R.attr.alertDialogTheme, - outValue, true); - return outValue.resourceId; - } - - private MenuType[] mMenuTypes; - class MenuType { - private int mMenuType; - - /** The layout inflater that uses the menu type's theme */ - private LayoutInflater mInflater; + private boolean mIsClosing = false; - /** The lazily loaded {@link MenuView} */ - private WeakReference<MenuView> mMenuView; + private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>(); - MenuType(int menuType) { - mMenuType = menuType; - } - - LayoutInflater getInflater() { - // Create an inflater that uses the given theme for the Views it inflates - if (mInflater == null) { - Context wrappedContext; - int themeResForType = THEME_RES_FOR_TYPE[mMenuType]; - switch (themeResForType) { - case THEME_APPLICATION: - wrappedContext = mContext; - break; - case THEME_ALERT_DIALOG: - wrappedContext = new ContextThemeWrapper(mContext, - getAlertDialogTheme(mContext)); - break; - default: - wrappedContext = new ContextThemeWrapper(mContext, themeResForType); - break; - } - mInflater = (LayoutInflater) wrappedContext - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - return mInflater; - } - - MenuView getMenuView(ViewGroup parent) { - if (LAYOUT_RES_FOR_TYPE[mMenuType] == 0) { - return null; - } - - synchronized (this) { - MenuView menuView = mMenuView != null ? mMenuView.get() : null; - - if (menuView == null) { - menuView = (MenuView) getInflater().inflate( - LAYOUT_RES_FOR_TYPE[mMenuType], parent, false); - menuView.initialize(MenuBuilder.this, mMenuType); - - // Cache the view - mMenuView = new WeakReference<MenuView>(menuView); - - if (mFrozenViewStates != null) { - View view = (View) menuView; - view.restoreHierarchyState(mFrozenViewStates); - - // Clear this menu type's frozen state, since we just restored it - mFrozenViewStates.remove(view.getId()); - } - } - - return menuView; - } - } - - boolean hasMenuView() { - return mMenuView != null && mMenuView.get() != null; - } - } + private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters = + new CopyOnWriteArrayList<WeakReference<MenuPresenter>>(); /** - * Called by menu to notify of close and selection changes + * Called by menu to notify of close and selection changes. */ public interface Callback { /** @@ -315,30 +160,6 @@ public class MenuBuilder implements Menu { public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item); /** - * Called when a menu is closed. - * @param menu The menu that was closed. - * @param allMenusAreClosing Whether the menus are completely closing (true), - * or whether there is another menu opening shortly - * (false). For example, if the menu is closing because a - * sub menu is about to be shown, <var>allMenusAreClosing</var> - * is false. - */ - public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing); - - /** - * Called when a sub menu is selected. This is a cue to open the given sub menu's decor. - * @param subMenu the sub menu that is being opened - * @return whether the sub menu selection was handled by the callback - */ - public boolean onSubMenuSelected(SubMenuBuilder subMenu); - - /** - * Called when a sub menu is closed - * @param menu the sub menu that was closed - */ - public void onCloseSubMenu(SubMenuBuilder menu); - - /** * Called when the mode of the menu changes (for example, from icon to expanded). * * @param menu the menu that has changed modes @@ -354,8 +175,6 @@ public class MenuBuilder implements Menu { } public MenuBuilder(Context context) { - mMenuTypes = new MenuType[NUM_TYPES]; - mContext = context; mResources = context.getResources(); @@ -375,82 +194,68 @@ public class MenuBuilder implements Menu { mDefaultShowAsAction = defaultShowAsAction; return this; } - - public void setCallback(Callback callback) { - mCallback = callback; - } - MenuType getMenuType(int menuType) { - if (mMenuTypes[menuType] == null) { - mMenuTypes[menuType] = new MenuType(menuType); - } - - return mMenuTypes[menuType]; + /** + * Add a presenter to this menu. This will only hold a WeakReference; + * you do not need to explicitly remove a presenter, but you can using + * {@link #removeMenuPresenter(MenuPresenter)}. + * + * @param presenter The presenter to add + */ + public void addMenuPresenter(MenuPresenter presenter) { + mPresenters.add(new WeakReference<MenuPresenter>(presenter)); + presenter.initForMenu(mContext, this); + mIsActionItemsStale = true; } - + /** - * Gets a menu View that contains this menu's items. - * - * @param menuType The type of menu to get a View for (must be one of - * {@link #TYPE_ICON}, {@link #TYPE_EXPANDED}, - * {@link #TYPE_DIALOG}). - * @param parent The ViewGroup that provides a set of LayoutParams values - * for this menu view - * @return A View for the menu of type <var>menuType</var> + * Remove a presenter from this menu. That presenter will no longer + * receive notifications of updates to this menu's data. + * + * @param presenter The presenter to remove */ - public View getMenuView(int menuType, ViewGroup parent) { - // The expanded menu depends on the number if items shown in the icon menu (which - // is adjustable as setters/XML attributes on IconMenuView [imagine a larger LCD - // wanting to show more icons]). If, for example, the activity goes through - // an orientation change while the expanded menu is open, the icon menu's view - // won't have an instance anymore; so here we make sure we have an icon menu view (matching - // the same parent so the layout parameters from the XML are used). This - // will create the icon menu view and cache it (if it doesn't already exist). - if (menuType == TYPE_EXPANDED - && (mMenuTypes[TYPE_ICON] == null || !mMenuTypes[TYPE_ICON].hasMenuView())) { - getMenuType(TYPE_ICON).getMenuView(parent); + public void removeMenuPresenter(MenuPresenter presenter) { + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter item = ref.get(); + if (item == null || item == presenter) { + mPresenters.remove(ref); + } } - - return (View) getMenuType(menuType).getMenuView(parent); } - private int getNumIconMenuItemsShown() { - ViewGroup parent = null; - - if (!mMenuTypes[TYPE_ICON].hasMenuView()) { - /* - * There isn't an icon menu view instantiated, so when we get it - * below, it will lazily instantiate it. We should pass a proper - * parent so it uses the layout_ attributes present in the XML - * layout file. - */ - if (mMenuTypes[TYPE_EXPANDED].hasMenuView()) { - View expandedMenuView = (View) mMenuTypes[TYPE_EXPANDED].getMenuView(null); - parent = (ViewGroup) expandedMenuView.getParent(); + private void dispatchPresenterUpdate(boolean cleared) { + if (mPresenters.isEmpty()) return; + + stopDispatchingItemsChanged(); + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else { + presenter.updateMenuView(cleared); } } - - return ((IconMenuView) getMenuView(TYPE_ICON, parent)).getNumActualItemsShown(); + startDispatchingItemsChanged(); } - /** - * Clears the cached menu views. Call this if the menu views need to another - * layout (for example, if the screen size has changed). - */ - public void clearMenuViews() { - for (int i = NUM_TYPES - 1; i >= 0; i--) { - if (mMenuTypes[i] != null) { - mMenuTypes[i].mMenuView = null; - } - } - - for (int i = mItems.size() - 1; i >= 0; i--) { - MenuItemImpl item = mItems.get(i); - if (item.hasSubMenu()) { - ((SubMenuBuilder) item.getSubMenu()).clearMenuViews(); + private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu) { + if (mPresenters.isEmpty()) return false; + + boolean result = false; + + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else if (!result) { + result = presenter.onSubMenuSelected(subMenu); } - item.clearItemViews(); } + return result; + } + + public void setCallback(Callback cb) { + mCallback = cb; } /** @@ -468,7 +273,7 @@ public class MenuBuilder implements Menu { } mItems.add(findInsertIndex(mItems, ordering), item); - onItemsChanged(false); + onItemsChanged(true); return item; } @@ -554,7 +359,7 @@ public class MenuBuilder implements Menu { } // Notify menu views - onItemsChanged(false); + onItemsChanged(true); } } @@ -573,7 +378,7 @@ public class MenuBuilder implements Menu { mItems.remove(index); - if (updateChildrenOnMenuViews) onItemsChanged(false); + if (updateChildrenOnMenuViews) onItemsChanged(true); } public void removeItemAt(int index) { @@ -585,6 +390,7 @@ public class MenuBuilder implements Menu { clear(); clearHeader(); mPreventDispatchingItemsChanged = false; + mItemsChangedWhileDispatchPrevented = false; onItemsChanged(true); } @@ -725,19 +531,14 @@ public class MenuBuilder implements Menu { return mItems.get(index); } - public MenuItem getOverflowItem(int index) { - flagActionItems(true); - return mNonActionItems.get(index); - } - public boolean isShortcutKey(int keyCode, KeyEvent event) { return findItemWithShortcutForKey(keyCode, event) != null; } public void setQwertyMode(boolean isQwerty) { mQwertyMode = isQwerty; - - refreshShortcuts(isShortcutsVisible(), isQwerty); + + onItemsChanged(false); } /** @@ -751,8 +552,7 @@ public class MenuBuilder implements Menu { * @return An ordering integer that can be used to order this item across * all the items (even from other categories). */ - private static int getOrdering(int categoryOrder) - { + private static int getOrdering(int categoryOrder) { final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT; if (index < 0 || index >= sCategoryToOrder.length) { @@ -770,23 +570,6 @@ public class MenuBuilder implements Menu { } /** - * Refreshes the shortcut labels on each of the displayed items. Passes the arguments - * so submenus don't need to call their parent menu for the same values. - */ - private void refreshShortcuts(boolean shortcutsVisible, boolean qwertyMode) { - MenuItemImpl item; - for (int i = mItems.size() - 1; i >= 0; i--) { - item = mItems.get(i); - - if (item.hasSubMenu()) { - ((MenuBuilder) item.getSubMenu()).refreshShortcuts(shortcutsVisible, qwertyMode); - } - - item.refreshShortcutOnItemViews(shortcutsVisible, qwertyMode); - } - } - - /** * Sets whether the shortcuts should be visible on menus. Devices without hardware * key input will never make shortcuts visible even if this method is passed 'true'. * @@ -798,7 +581,7 @@ public class MenuBuilder implements Menu { if (mShortcutsVisible == shortcutsVisible) return; setShortcutsVisibleInner(shortcutsVisible); - refreshShortcuts(mShortcutsVisible, isQwertyMode()); + onItemsChanged(false); } private void setShortcutsVisibleInner(boolean shortcutsVisible) { @@ -818,15 +601,24 @@ public class MenuBuilder implements Menu { Resources getResources() { return mResources; } - - public Callback getCallback() { - return mCallback; - } public Context getContext() { return mContext; } + boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) { + return mCallback != null && mCallback.onMenuItemSelected(menu, item); + } + + /** + * Dispatch a mode change event to this menu's callback. + */ + public void changeMenuMode() { + if (mCallback != null) { + mCallback.onMenuModeChange(this); + } + } + private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) { for (int i = items.size() - 1; i >= 0; i--) { MenuItemImpl item = items.get(i); @@ -860,7 +652,7 @@ public class MenuBuilder implements Menu { * (the ALT-enabled char corresponds to the shortcut) associated * with the keyCode. */ - List<MenuItemImpl> findItemsWithShortcutForKey(int keyCode, KeyEvent event) { + void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) { final boolean qwerty = isQwertyMode(); final int metaState = event.getMetaState(); final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); @@ -868,18 +660,15 @@ public class MenuBuilder implements Menu { final boolean isKeyCodeMapped = event.getKeyData(possibleChars); // The delete key is not mapped to '\b' so we treat it specially if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) { - return null; + return; } - Vector<MenuItemImpl> items = new Vector(); // Look for an item whose shortcut is this key. final int N = mItems.size(); for (int i = 0; i < N; i++) { MenuItemImpl item = mItems.get(i); if (item.hasSubMenu()) { - List<MenuItemImpl> subMenuItems = ((MenuBuilder)item.getSubMenu()) - .findItemsWithShortcutForKey(keyCode, event); - items.addAll(subMenuItems); + ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event); } final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut(); if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) && @@ -892,7 +681,6 @@ public class MenuBuilder implements Menu { items.add(item); } } - return items; } /* @@ -908,9 +696,11 @@ public class MenuBuilder implements Menu { */ MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) { // Get all items that can be associated directly or indirectly with the keyCode - List<MenuItemImpl> items = findItemsWithShortcutForKey(keyCode, event); + ArrayList<MenuItemImpl> items = mTempShortcutItemList; + items.clear(); + findItemsWithShortcutForKey(items, keyCode, event); - if (items == null) { + if (items.isEmpty()) { return null; } @@ -920,15 +710,18 @@ public class MenuBuilder implements Menu { event.getKeyData(possibleChars); // If we have only one element, we can safely returns it - if (items.size() == 1) { + final int size = items.size(); + if (size == 1) { return items.get(0); } final boolean qwerty = isQwertyMode(); // If we found more than one item associated with the key, // we have to return the exact match - for (MenuItemImpl item : items) { - final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut(); + for (int i = 0; i < size; i++) { + final MenuItemImpl item = items.get(i); + final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : + item.getNumericShortcut(); if ((shortcutChar == possibleChars.meta[0] && (metaState & KeyEvent.META_ALT_ON) == 0) || (shortcutChar == possibleChars.meta[2] && @@ -958,11 +751,8 @@ public class MenuBuilder implements Menu { if (item.hasSubMenu()) { close(false); - if (mCallback != null) { - // Return true if the sub menu was invoked or the item was invoked previously - invoked = mCallback.onSubMenuSelected((SubMenuBuilder) item.getSubMenu()) - || invoked; - } + invoked |= dispatchSubMenuSelected((SubMenuBuilder) item.getSubMenu()); + if (!invoked) close(true); } else { if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) { close(true); @@ -982,10 +772,18 @@ public class MenuBuilder implements Menu { * is false. */ final void close(boolean allMenusAreClosing) { - Callback callback = getCallback(); - if (callback != null) { - callback.onCloseMenu(this, allMenusAreClosing); + if (mIsClosing) return; + + mIsClosing = true; + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else { + presenter.onCloseMenu(this, allMenusAreClosing); + } } + mIsClosing = false; } /** {@inheritDoc} */ @@ -996,26 +794,40 @@ public class MenuBuilder implements Menu { /** * Called when an item is added or removed. * - * @param cleared Whether the items were cleared or just changed. + * @param structureChanged true if the menu structure changed, + * false if only item properties changed. */ - private void onItemsChanged(boolean cleared) { + void onItemsChanged(boolean structureChanged) { if (!mPreventDispatchingItemsChanged) { - if (mIsVisibleItemsStale == false) mIsVisibleItemsStale = true; - if (mIsActionItemsStale == false) mIsActionItemsStale = true; - - MenuType[] menuTypes = mMenuTypes; - for (int i = 0; i < NUM_TYPES; i++) { - if ((menuTypes[i] != null) && (menuTypes[i].hasMenuView())) { - MenuView menuView = menuTypes[i].mMenuView.get(); - menuView.updateChildren(cleared); - } + if (structureChanged) { + mIsVisibleItemsStale = true; + mIsActionItemsStale = true; + } - MenuAdapter adapter = mAdapterCache[i] == null ? null : mAdapterCache[i].get(); - if (adapter != null) adapter.notifyDataSetChanged(); + dispatchPresenterUpdate(structureChanged); + } else { + mItemsChangedWhileDispatchPrevented = true; + } + } - adapter = mOverflowAdapterCache[i] == null ? null : mOverflowAdapterCache[i].get(); - if (adapter != null) adapter.notifyDataSetChanged(); - } + /** + * Stop dispatching item changed events to presenters until + * {@link #startDispatchingItemsChanged()} is called. Useful when + * many menu operations are going to be performed as a batch. + */ + public void stopDispatchingItemsChanged() { + if (!mPreventDispatchingItemsChanged) { + mPreventDispatchingItemsChanged = true; + mItemsChangedWhileDispatchPrevented = false; + } + } + + public void startDispatchingItemsChanged() { + mPreventDispatchingItemsChanged = false; + + if (mItemsChangedWhileDispatchPrevented) { + mItemsChangedWhileDispatchPrevented = false; + onItemsChanged(true); } } @@ -1025,6 +837,7 @@ public class MenuBuilder implements Menu { */ void onItemVisibleChanged(MenuItemImpl item) { // Notify of items being changed + mIsVisibleItemsStale = true; onItemsChanged(false); } @@ -1034,6 +847,7 @@ public class MenuBuilder implements Menu { */ void onItemActionRequestChanged(MenuItemImpl item) { // Notify of items being changed + mIsActionItemsStale = true; onItemsChanged(false); } @@ -1055,17 +869,6 @@ public class MenuBuilder implements Menu { return mVisibleItems; } - - /** - * @return A fake action button parent view for obtaining child views. - */ - private ViewGroup getMeasureActionButtonParent() { - if (mMeasureActionButtonParent == null) { - mMeasureActionButtonParent = (ViewGroup) getMenuType(TYPE_ACTION_BUTTON).getInflater() - .inflate(LAYOUT_RES_FOR_TYPE[TYPE_ACTION_BUTTON], null, false); - } - return mMeasureActionButtonParent; - } /** * This method determines which menu items get to be 'action items' that will appear @@ -1090,147 +893,56 @@ public class MenuBuilder implements Menu { * <p>The space freed by demoting a full group cannot be consumed by future menu items. * Once items begin to overflow, all future items become overflow items as well. This is * to avoid inadvertent reordering that may break the app's intended design. - * - * @param reserveActionOverflow true if an overflow button should consume one space - * in the available item count */ - private void flagActionItems(boolean reserveActionOverflow) { - if (reserveActionOverflow != mReserveActionOverflow) { - mReserveActionOverflow = reserveActionOverflow; - mIsActionItemsStale = true; - } - + public void flagActionItems() { if (!mIsActionItemsStale) { return; } - final ArrayList<MenuItemImpl> visibleItems = getVisibleItems(); - final int itemsSize = visibleItems.size(); - int maxActions = mMaxActionItems; - int widthLimit = mActionWidthLimit; - final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - final ViewGroup parent = getMeasureActionButtonParent(); - - int requiredItems = 0; - int requestedItems = 0; - int firstActionWidth = 0; - boolean hasOverflow = false; - for (int i = 0; i < itemsSize; i++) { - MenuItemImpl item = visibleItems.get(i); - if (item.requiresActionButton()) { - requiredItems++; - } else if (item.requestsActionButton()) { - requestedItems++; + // Presenters flag action items as needed. + boolean flagged = false; + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); } else { - hasOverflow = true; + flagged |= presenter.flagActionItems(); } } - // Reserve a spot for the overflow item if needed. - if (reserveActionOverflow && - (hasOverflow || requiredItems + requestedItems > maxActions)) { - maxActions--; - } - maxActions -= requiredItems; - - final SparseBooleanArray seenGroups = mActionButtonGroups; - seenGroups.clear(); - - // Flag as many more requested items as will fit. - for (int i = 0; i < itemsSize; i++) { - MenuItemImpl item = visibleItems.get(i); - - if (item.requiresActionButton()) { - View v = item.getActionView(); - if (v == null) { - v = item.getItemView(TYPE_ACTION_BUTTON, parent); - } - v.measure(querySpec, querySpec); - final int measuredWidth = v.getMeasuredWidth(); - widthLimit -= measuredWidth; - if (firstActionWidth == 0) { - firstActionWidth = measuredWidth; - } - final int groupId = item.getGroupId(); - if (groupId != 0) { - seenGroups.put(groupId, true); - } - } else if (item.requestsActionButton()) { - // Items in a group with other items that already have an action slot - // can break the max actions rule, but not the width limit. - final int groupId = item.getGroupId(); - final boolean inGroup = seenGroups.get(groupId); - boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0; - maxActions--; - - if (isAction) { - View v = item.getActionView(); - if (v == null) { - v = item.getItemView(TYPE_ACTION_BUTTON, parent); - } - v.measure(querySpec, querySpec); - final int measuredWidth = v.getMeasuredWidth(); - widthLimit -= measuredWidth; - if (firstActionWidth == 0) { - firstActionWidth = measuredWidth; - } - - // Did this push the entire first item past halfway? - if (widthLimit + firstActionWidth <= 0) { - isAction = false; - } - } - - if (isAction && groupId != 0) { - seenGroups.put(groupId, true); - } else if (inGroup) { - // We broke the width limit. Demote the whole group, they all overflow now. - seenGroups.put(groupId, false); - for (int j = 0; j < i; j++) { - MenuItemImpl areYouMyGroupie = visibleItems.get(j); - if (areYouMyGroupie.getGroupId() == groupId) { - areYouMyGroupie.setIsActionButton(false); - } - } + if (flagged) { + mActionItems.clear(); + mNonActionItems.clear(); + ArrayList<MenuItemImpl> visibleItems = getVisibleItems(); + final int itemsSize = visibleItems.size(); + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + if (item.isActionButton()) { + mActionItems.add(item); + } else { + mNonActionItems.add(item); } - - item.setIsActionButton(isAction); } + } else if (mActionItems.size() + mNonActionItems.size() != getVisibleItems().size()) { + // Nobody flagged anything, but if something doesn't add up then treat everything + // as non-action items. + // (This happens during a first pass with no action-item presenters.) + mActionItems.clear(); + mNonActionItems.clear(); + mNonActionItems.addAll(getVisibleItems()); } - - mActionItems.clear(); - mNonActionItems.clear(); - for (int i = 0; i < itemsSize; i++) { - MenuItemImpl item = visibleItems.get(i); - if (item.isActionButton()) { - mActionItems.add(item); - } else { - mNonActionItems.add(item); - } - } - mIsActionItemsStale = false; } - ArrayList<MenuItemImpl> getActionItems(boolean reserveActionOverflow) { - flagActionItems(reserveActionOverflow); + ArrayList<MenuItemImpl> getActionItems() { + flagActionItems(); return mActionItems; } - ArrayList<MenuItemImpl> getNonActionItems(boolean reserveActionOverflow) { - flagActionItems(reserveActionOverflow); + ArrayList<MenuItemImpl> getNonActionItems() { + flagActionItems(); return mNonActionItems; } - - void setMaxActionItems(int maxActionItems) { - mMaxActionItems = maxActionItems; - mIsActionItemsStale = true; - } - - void setActionWidthLimit(int widthLimit) { - mActionWidthLimit = widthLimit; - mIsActionItemsStale = true; - } public void clearHeader() { mHeaderIcon = null; @@ -1362,38 +1074,6 @@ public class MenuBuilder implements Menu { mCurrentMenuInfo = menuInfo; } - /** - * Gets an adapter for providing items and their views. - * - * @param menuType The type of menu to get an adapter for. - * @return A {@link MenuAdapter} for this menu with the given menu type. - */ - public MenuAdapter getMenuAdapter(int menuType) { - MenuAdapter adapter = mAdapterCache[menuType] == null ? - null : mAdapterCache[menuType].get(); - if (adapter != null) return adapter; - - adapter = new MenuAdapter(menuType); - mAdapterCache[menuType] = new WeakReference<MenuAdapter>(adapter); - return adapter; - } - - /** - * Gets an adapter for providing overflow (non-action) items and their views. - * - * @param menuType The type of menu to get an adapter for. - * @return A {@link MenuAdapter} for this menu with the given menu type. - */ - public MenuAdapter getOverflowMenuAdapter(int menuType) { - OverflowMenuAdapter adapter = mOverflowAdapterCache[menuType] == null ? - null : mOverflowAdapterCache[menuType].get(); - if (adapter != null) return adapter; - - adapter = new OverflowMenuAdapter(menuType); - mOverflowAdapterCache[menuType] = new WeakReference<OverflowMenuAdapter>(adapter); - return adapter; - } - void setOptionalIconsVisible(boolean visible) { mOptionalIconsVisible = visible; } @@ -1401,109 +1081,4 @@ public class MenuBuilder implements Menu { boolean getOptionalIconsVisible() { return mOptionalIconsVisible; } - - public void saveHierarchyState(Bundle outState) { - SparseArray<Parcelable> viewStates = new SparseArray<Parcelable>(); - - MenuType[] menuTypes = mMenuTypes; - for (int i = NUM_TYPES - 1; i >= 0; i--) { - if (menuTypes[i] == null) { - continue; - } - - if (menuTypes[i].hasMenuView()) { - ((View) menuTypes[i].getMenuView(null)).saveHierarchyState(viewStates); - } - } - - outState.putSparseParcelableArray(VIEWS_TAG, viewStates); - } - - public void restoreHierarchyState(Bundle inState) { - // Save this for menu views opened later - SparseArray<Parcelable> viewStates = mFrozenViewStates = inState - .getSparseParcelableArray(VIEWS_TAG); - - // Thaw those menu views already open - MenuType[] menuTypes = mMenuTypes; - for (int i = NUM_TYPES - 1; i >= 0; i--) { - if (menuTypes[i] == null) { - continue; - } - - if (menuTypes[i].hasMenuView()) { - ((View) menuTypes[i].getMenuView(null)).restoreHierarchyState(viewStates); - } - } - } - - /** - * An adapter that allows an {@link AdapterView} to use this {@link MenuBuilder} as a data - * source. This adapter will use only the visible/shown items from the menu. - */ - public class MenuAdapter extends BaseAdapter { - private int mMenuType; - - public MenuAdapter(int menuType) { - mMenuType = menuType; - } - - public int getOffset() { - if (mMenuType == TYPE_EXPANDED) { - return getNumIconMenuItemsShown(); - } else { - return 0; - } - } - - public int getCount() { - return getVisibleItems().size() - getOffset(); - } - - public MenuItemImpl getItem(int position) { - return getVisibleItems().get(position + getOffset()); - } - - public long getItemId(int position) { - // Since a menu item's ID is optional, we'll use the position as an - // ID for the item in the AdapterView - return position; - } - - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView != null) { - MenuView.ItemView itemView = (MenuView.ItemView) convertView; - itemView.getItemData().setItemView(mMenuType, null); - - MenuItemImpl item = (MenuItemImpl) getItem(position); - itemView.initialize(item, mMenuType); - item.setItemView(mMenuType, itemView); - return convertView; - } else { - MenuItemImpl item = (MenuItemImpl) getItem(position); - item.setItemView(mMenuType, null); - return item.getItemView(mMenuType, parent); - } - } - } - - /** - * An adapter that allows an {@link AdapterView} to use this {@link MenuBuilder} as a data - * source for overflow menu items that do not fit in the list of action items. - */ - private class OverflowMenuAdapter extends MenuAdapter { - public OverflowMenuAdapter(int menuType) { - super(menuType); - } - - @Override - public MenuItemImpl getItem(int position) { - return getNonActionItems(true).get(position); - } - - @Override - public int getCount() { - return getNonActionItems(true).size(); - } - } } diff --git a/core/java/com/android/internal/view/menu/MenuDialogHelper.java b/core/java/com/android/internal/view/menu/MenuDialogHelper.java index d7438d6..5c8e057 100644 --- a/core/java/com/android/internal/view/menu/MenuDialogHelper.java +++ b/core/java/com/android/internal/view/menu/MenuDialogHelper.java @@ -24,17 +24,20 @@ import android.view.KeyEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; -import android.widget.ListAdapter; /** * Helper for menus that appear as Dialogs (context and submenus). * * @hide */ -public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogInterface.OnClickListener { +public class MenuDialogHelper implements DialogInterface.OnKeyListener, + DialogInterface.OnClickListener, + DialogInterface.OnDismissListener, + MenuPresenter.Callback { private MenuBuilder mMenu; - private ListAdapter mAdapter; private AlertDialog mDialog; + ListMenuPresenter mPresenter; + private MenuPresenter.Callback mPresenterCallback; public MenuDialogHelper(MenuBuilder menu) { mMenu = menu; @@ -49,12 +52,15 @@ public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogIn // Many references to mMenu, create local reference final MenuBuilder menu = mMenu; - // Get an adapter for the menu item views - mAdapter = menu.getMenuAdapter(MenuBuilder.TYPE_DIALOG); - // Get the builder for the dialog - final AlertDialog.Builder builder = new AlertDialog.Builder(menu.getContext()) - .setAdapter(mAdapter, this); + final AlertDialog.Builder builder = new AlertDialog.Builder(menu.getContext()); + + mPresenter = new ListMenuPresenter(builder.getContext(), + com.android.internal.R.layout.list_menu_item_layout); + + mPresenter.setCallback(this); + mMenu.addMenuPresenter(mPresenter); + builder.setAdapter(mPresenter.getAdapter(), this); // Set the title final View headerView = menu.getHeaderView(); @@ -68,13 +74,10 @@ public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogIn // Set the key listener builder.setOnKeyListener(this); - - // Since this is for a menu, disable the recycling of views - // This is done by the menu framework anyway - builder.setRecycleOnMeasureEnabled(false); // Show the menu mDialog = builder.create(); + mDialog.setOnDismissListener(this); WindowManager.LayoutParams lp = mDialog.getWindow().getAttributes(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; @@ -122,6 +125,10 @@ public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogIn } + public void setPresenterCallback(MenuPresenter.Callback cb) { + mPresenterCallback = cb; + } + /** * Dismisses the menu's dialog. * @@ -132,9 +139,31 @@ public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogIn mDialog.dismiss(); } } - + + @Override + public void onDismiss(DialogInterface dialog) { + mPresenter.onCloseMenu(mMenu, true); + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + if (allMenusAreClosing || menu == mMenu) { + dismiss(); + } + if (mPresenterCallback != null) { + mPresenterCallback.onCloseMenu(menu, allMenusAreClosing); + } + } + + @Override + public boolean onOpenSubMenu(MenuBuilder subMenu) { + if (mPresenterCallback != null) { + return mPresenterCallback.onOpenSubMenu(subMenu); + } + return false; + } + public void onClick(DialogInterface dialog, int which) { - mMenu.performItemAction((MenuItemImpl) mAdapter.getItem(which), 0); + mMenu.performItemAction((MenuItemImpl) mPresenter.getAdapter().getItem(which), 0); } - } diff --git a/core/java/com/android/internal/view/menu/MenuItemImpl.java b/core/java/com/android/internal/view/menu/MenuItemImpl.java index 305115f..c6d386d 100644 --- a/core/java/com/android/internal/view/menu/MenuItemImpl.java +++ b/core/java/com/android/internal/view/menu/MenuItemImpl.java @@ -16,21 +16,18 @@ package com.android.internal.view.menu; -import java.lang.ref.WeakReference; +import com.android.internal.view.menu.MenuView.ItemView; import android.content.ActivityNotFoundException; import android.content.Intent; import android.graphics.drawable.Drawable; import android.util.Log; +import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import android.view.ViewDebug; -import android.view.ViewGroup; -import android.view.ContextMenu.ContextMenuInfo; - -import com.android.internal.view.menu.MenuView.ItemView; /** * @hide @@ -60,9 +57,6 @@ public final class MenuItemImpl implements MenuItem { * needed). */ private int mIconResId = NO_ICON; - - /** The (cached) menu item views for this item */ - private WeakReference<ItemView> mItemViews[]; /** The menu to which this item belongs */ private MenuBuilder mMenu; @@ -128,7 +122,6 @@ public final class MenuItemImpl implements MenuItem { com.android.internal.R.string.menu_space_shortcut_label); } - mItemViews = new WeakReference[MenuBuilder.NUM_TYPES]; mMenu = menu; mId = id; mGroup = group; @@ -149,9 +142,7 @@ public final class MenuItemImpl implements MenuItem { return true; } - MenuBuilder.Callback callback = mMenu.getCallback(); - if (callback != null && - callback.onMenuItemSelected(mMenu.getRootMenu(), this)) { + if (mMenu.dispatchMenuItemSelected(mMenu.getRootMenu(), this)) { return true; } @@ -172,10 +163,6 @@ public final class MenuItemImpl implements MenuItem { return false; } - private boolean hasItemView(int menuType) { - return mItemViews[menuType] != null && mItemViews[menuType].get() != null; - } - public boolean isEnabled() { return (mFlags & ENABLED) != 0; } @@ -187,13 +174,7 @@ public final class MenuItemImpl implements MenuItem { mFlags &= ~ENABLED; } - for (int i = MenuBuilder.NUM_TYPES - 1; i >= 0; i--) { - // If the item view prefers a condensed title, only set this title if there - // is no condensed title for this item - if (hasItemView(i)) { - mItemViews[i].get().setEnabled(enabled); - } - } + mMenu.onItemsChanged(false); return this; } @@ -242,7 +223,7 @@ public final class MenuItemImpl implements MenuItem { mShortcutAlphabeticChar = Character.toLowerCase(alphaChar); - refreshShortcutOnItemViews(); + mMenu.onItemsChanged(false); return this; } @@ -256,7 +237,7 @@ public final class MenuItemImpl implements MenuItem { mShortcutNumericChar = numericChar; - refreshShortcutOnItemViews(); + mMenu.onItemsChanged(false); return this; } @@ -265,7 +246,7 @@ public final class MenuItemImpl implements MenuItem { mShortcutNumericChar = numericChar; mShortcutAlphabeticChar = Character.toLowerCase(alphaChar); - refreshShortcutOnItemViews(); + mMenu.onItemsChanged(false); return this; } @@ -322,38 +303,6 @@ public final class MenuItemImpl implements MenuItem { return mMenu.isShortcutsVisible() && (getShortcut() != 0); } - /** - * Refreshes the shortcut shown on the ItemViews. This method retrieves current - * shortcut state (mode and shown) from the menu that contains this item. - */ - private void refreshShortcutOnItemViews() { - refreshShortcutOnItemViews(mMenu.isShortcutsVisible(), mMenu.isQwertyMode()); - } - - /** - * Refreshes the shortcut shown on the ItemViews. This is usually called by - * the {@link MenuBuilder} when it is refreshing the shortcuts on all item - * views, so it passes arguments rather than each item calling a method on the menu to get - * the same values. - * - * @param menuShortcutShown The menu's shortcut shown mode. In addition, - * this method will ensure this item has a shortcut before it - * displays the shortcut. - * @param isQwertyMode Whether the shortcut mode is qwerty mode - */ - void refreshShortcutOnItemViews(boolean menuShortcutShown, boolean isQwertyMode) { - final char shortcutKey = (isQwertyMode) ? mShortcutAlphabeticChar : mShortcutNumericChar; - - // Show shortcuts if the menu is supposed to show shortcuts AND this item has a shortcut - final boolean showShortcut = menuShortcutShown && (shortcutKey != 0); - - for (int i = MenuBuilder.NUM_TYPES - 1; i >= 0; i--) { - if (hasItemView(i)) { - mItemViews[i].get().setShortcut(showShortcut, shortcutKey); - } - } - } - public SubMenu getSubMenu() { return mSubMenu; } @@ -394,18 +343,7 @@ public final class MenuItemImpl implements MenuItem { public MenuItem setTitle(CharSequence title) { mTitle = title; - for (int i = MenuBuilder.NUM_TYPES - 1; i >= 0; i--) { - // If the item view prefers a condensed title, only set this title if there - // is no condensed title for this item - if (!hasItemView(i)) { - continue; - } - - ItemView itemView = mItemViews[i].get(); - if (!itemView.prefersCondensedTitle() || mTitleCondensed == null) { - itemView.setTitle(title); - } - } + mMenu.onItemsChanged(false); if (mSubMenu != null) { mSubMenu.setHeaderTitle(title); @@ -430,18 +368,12 @@ public final class MenuItemImpl implements MenuItem { title = mTitle; } - for (int i = MenuBuilder.NUM_TYPES - 1; i >= 0; i--) { - // Refresh those item views that prefer a condensed title - if (hasItemView(i) && (mItemViews[i].get().prefersCondensedTitle())) { - mItemViews[i].get().setTitle(title); - } - } + mMenu.onItemsChanged(false); return this; } public Drawable getIcon() { - if (mIconDrawable != null) { return mIconDrawable; } @@ -456,7 +388,7 @@ public final class MenuItemImpl implements MenuItem { public MenuItem setIcon(Drawable icon) { mIconResId = NO_ICON; mIconDrawable = icon; - setIconOnViews(icon); + mMenu.onItemsChanged(false); return this; } @@ -466,33 +398,10 @@ public final class MenuItemImpl implements MenuItem { mIconResId = iconResId; // If we have a view, we need to push the Drawable to them - if (haveAnyOpenedIconCapableItemViews()) { - Drawable drawable = iconResId != NO_ICON ? mMenu.getResources().getDrawable(iconResId) - : null; - setIconOnViews(drawable); - } + mMenu.onItemsChanged(false); return this; } - - private void setIconOnViews(Drawable icon) { - for (int i = MenuBuilder.NUM_TYPES - 1; i >= 0; i--) { - // Refresh those item views that are able to display an icon - if (hasItemView(i) && mItemViews[i].get().showsIcon()) { - mItemViews[i].get().setIcon(icon); - } - } - } - - private boolean haveAnyOpenedIconCapableItemViews() { - for (int i = MenuBuilder.NUM_TYPES - 1; i >= 0; i--) { - if (hasItemView(i) && mItemViews[i].get().showsIcon()) { - return true; - } - } - - return false; - } public boolean isCheckable() { return (mFlags & CHECKABLE) == CHECKABLE; @@ -502,19 +411,14 @@ public final class MenuItemImpl implements MenuItem { final int oldFlags = mFlags; mFlags = (mFlags & ~CHECKABLE) | (checkable ? CHECKABLE : 0); if (oldFlags != mFlags) { - for (int i = MenuBuilder.NUM_TYPES - 1; i >= 0; i--) { - if (hasItemView(i)) { - mItemViews[i].get().setCheckable(checkable); - } - } + mMenu.onItemsChanged(false); } return this; } - public void setExclusiveCheckable(boolean exclusive) - { - mFlags = (mFlags&~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0); + public void setExclusiveCheckable(boolean exclusive) { + mFlags = (mFlags & ~EXCLUSIVE) | (exclusive ? EXCLUSIVE : 0); } public boolean isExclusiveCheckable() { @@ -541,11 +445,7 @@ public final class MenuItemImpl implements MenuItem { final int oldFlags = mFlags; mFlags = (mFlags & ~CHECKED) | (checked ? CHECKED : 0); if (oldFlags != mFlags) { - for (int i = MenuBuilder.NUM_TYPES - 1; i >= 0; i--) { - if (hasItemView(i)) { - mItemViews[i].get().setChecked(checked); - } - } + mMenu.onItemsChanged(false); } } @@ -581,39 +481,6 @@ public final class MenuItemImpl implements MenuItem { mClickListener = clickListener; return this; } - - View getItemView(int menuType, ViewGroup parent) { - if (!hasItemView(menuType)) { - mItemViews[menuType] = new WeakReference<ItemView>(createItemView(menuType, parent)); - } - - return (View) mItemViews[menuType].get(); - } - - void setItemView(int menuType, ItemView view) { - mItemViews[menuType] = new WeakReference<ItemView>(view); - } - - /** - * Create and initializes a menu item view that implements {@link MenuView.ItemView}. - * @param menuType The type of menu to get a View for (must be one of - * {@link MenuBuilder#TYPE_ICON}, {@link MenuBuilder#TYPE_EXPANDED}, - * {@link MenuBuilder#TYPE_SUB}, {@link MenuBuilder#TYPE_CONTEXT}). - * @return The inflated {@link MenuView.ItemView} that is ready for use - */ - private MenuView.ItemView createItemView(int menuType, ViewGroup parent) { - // Create the MenuView - MenuView.ItemView itemView = (MenuView.ItemView) getLayoutInflater(menuType) - .inflate(MenuBuilder.ITEM_LAYOUT_RES_FOR_TYPE[menuType], parent, false); - itemView.initialize(this, menuType); - return itemView; - } - - void clearItemViews() { - for (int i = mItemViews.length - 1; i >= 0; i--) { - mItemViews[i] = null; - } - } @Override public String toString() { @@ -627,24 +494,12 @@ public final class MenuItemImpl implements MenuItem { public ContextMenuInfo getMenuInfo() { return mMenuInfo; } - - /** - * Returns a LayoutInflater that is themed for the given menu type. - * - * @param menuType The type of menu. - * @return A LayoutInflater. - */ - public LayoutInflater getLayoutInflater(int menuType) { - return mMenu.getMenuType(menuType).getInflater(); - } /** - * @return Whether the given menu type should show icons for menu items. + * @return Whether the menu should show icons for menu items. */ - public boolean shouldShowIcon(int menuType) { - return menuType == MenuBuilder.TYPE_ICON || - menuType == MenuBuilder.TYPE_ACTION_BUTTON || - mMenu.getOptionalIconsVisible(); + public boolean shouldShowIcon() { + return mMenu.getOptionalIconsVisible(); } public boolean isActionButton() { @@ -668,7 +523,9 @@ public final class MenuItemImpl implements MenuItem { } public boolean showsTextAsAction() { - return (mShowAsAction & SHOW_AS_ACTION_WITH_TEXT) == SHOW_AS_ACTION_WITH_TEXT; + return (mShowAsAction & SHOW_AS_ACTION_WITH_TEXT) == SHOW_AS_ACTION_WITH_TEXT && + mMenu.getContext().getResources().getBoolean( + com.android.internal.R.bool.allow_action_menu_item_text_with_icon); } public void setShowAsAction(int actionEnum) { @@ -696,8 +553,8 @@ public final class MenuItemImpl implements MenuItem { public MenuItem setActionView(int resId) { LayoutInflater inflater = LayoutInflater.from(mMenu.getContext()); - ViewGroup parent = (ViewGroup) mMenu.getMenuView(MenuBuilder.TYPE_ACTION_BUTTON, null); - setActionView(inflater.inflate(resId, parent, false)); + // TODO - Fix for proper parent. Lazily inflate in the presenter. + setActionView(inflater.inflate(resId, null)); return this; } diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java index 04a059e..38cec29 100644 --- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java +++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java @@ -16,31 +16,35 @@ package com.android.internal.view.menu; -import com.android.internal.view.menu.MenuBuilder.MenuAdapter; - import android.content.Context; -import android.os.Handler; import android.util.DisplayMetrics; import android.view.KeyEvent; -import android.view.MenuItem; +import android.view.LayoutInflater; import android.view.View; import android.view.View.MeasureSpec; +import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ListAdapter; import android.widget.ListPopupWindow; import android.widget.PopupWindow; -import java.lang.ref.WeakReference; +import java.util.ArrayList; /** + * Presents a menu as a small, simple popup anchored to another view. * @hide */ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.OnKeyListener, ViewTreeObserver.OnGlobalLayoutListener, PopupWindow.OnDismissListener, - View.OnAttachStateChangeListener { + View.OnAttachStateChangeListener, MenuPresenter { private static final String TAG = "MenuPopupHelper"; + static final int ITEM_LAYOUT = com.android.internal.R.layout.popup_menu_item_layout; + private Context mContext; + private LayoutInflater mInflater; private ListPopupWindow mPopup; private MenuBuilder mMenu; private int mPopupMaxWidth; @@ -48,7 +52,9 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On private boolean mOverflowOnly; private ViewTreeObserver mTreeObserver; - private final Handler mHandler = new Handler(); + private MenuAdapter mAdapter; + + private Callback mPresenterCallback; public MenuPopupHelper(Context context, MenuBuilder menu) { this(context, menu, null, false); @@ -61,6 +67,7 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On public MenuPopupHelper(Context context, MenuBuilder menu, View anchorView, boolean overflowOnly) { mContext = context; + mInflater = LayoutInflater.from(context); mMenu = menu; mOverflowOnly = overflowOnly; @@ -68,6 +75,8 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On mPopupMaxWidth = metrics.widthPixels / 2; mAnchorView = anchorView; + + menu.addMenuPresenter(this); } public void setAnchorView(View anchor) { @@ -82,23 +91,14 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On public boolean tryShow() { mPopup = new ListPopupWindow(mContext, null, com.android.internal.R.attr.popupMenuStyle); - mPopup.setOnItemClickListener(this); mPopup.setOnDismissListener(this); + mPopup.setOnItemClickListener(this); - final MenuAdapter adapter = mOverflowOnly ? - mMenu.getOverflowMenuAdapter(MenuBuilder.TYPE_POPUP) : - mMenu.getMenuAdapter(MenuBuilder.TYPE_POPUP); - mPopup.setAdapter(adapter); + mAdapter = new MenuAdapter(mMenu); + mPopup.setAdapter(mAdapter); mPopup.setModal(true); View anchor = mAnchorView; - if (anchor == null && mMenu instanceof SubMenuBuilder) { - SubMenuBuilder subMenu = (SubMenuBuilder) mMenu; - final MenuItemImpl itemImpl = (MenuItemImpl) subMenu.getItem(); - anchor = itemImpl.getItemView(MenuBuilder.TYPE_ACTION_BUTTON, null); - mAnchorView = anchor; - } - if (anchor != null) { final boolean addGlobalListener = mTreeObserver == null; mTreeObserver = anchor.getViewTreeObserver(); // Refresh to latest @@ -109,7 +109,7 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On return false; } - mPopup.setContentWidth(Math.min(measureContentWidth(adapter), mPopupMaxWidth)); + mPopup.setContentWidth(Math.min(measureContentWidth(mAdapter), mPopupMaxWidth)); mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); mPopup.show(); mPopup.getListView().setOnKeyListener(this); @@ -136,23 +136,10 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On return mPopup != null && mPopup.isShowing(); } + @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { - if (!isShowing()) return; - - MenuItem item = null; - if (mOverflowOnly) { - item = mMenu.getOverflowItem(position); - } else { - item = mMenu.getVisibleItems().get(position); - } - dismiss(); - - final MenuItem performItem = item; - mHandler.post(new Runnable() { - public void run() { - mMenu.performItemAction(performItem, 0); - } - }); + MenuAdapter adapter = mAdapter; + adapter.mAdapterMenu.performItemAction(adapter.getItem(position), 0); } public boolean onKey(View v, int keyCode, KeyEvent event) { @@ -163,7 +150,7 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On return false; } - private int measureContentWidth(MenuAdapter adapter) { + private int measureContentWidth(ListAdapter adapter) { // Menus don't tend to be long, so this is more sane than it looks. int width = 0; View itemView = null; @@ -211,4 +198,91 @@ public class MenuPopupHelper implements AdapterView.OnItemClickListener, View.On } v.removeOnAttachStateChangeListener(this); } + + @Override + public void initForMenu(Context context, MenuBuilder menu) { + // Don't need to do anything; we added as a presenter in the constructor. + } + + @Override + public MenuView getMenuView(ViewGroup root) { + throw new UnsupportedOperationException("MenuPopupHelpers manage their own views"); + } + + @Override + public void updateMenuView(boolean cleared) { + if (mAdapter != null) mAdapter.notifyDataSetChanged(); + } + + @Override + public void setCallback(Callback cb) { + mPresenterCallback = cb; + } + + @Override + public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + if (subMenu.hasVisibleItems()) { + MenuPopupHelper subPopup = new MenuPopupHelper(mContext, subMenu, mAnchorView, false); + subPopup.setCallback(mPresenterCallback); + if (subPopup.tryShow()) { + if (mPresenterCallback != null) { + mPresenterCallback.onOpenSubMenu(subMenu); + } + return true; + } + } + return false; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + // Only care about the (sub)menu we're presenting. + if (menu != mMenu) return; + + dismiss(); + if (mPresenterCallback != null) { + mPresenterCallback.onCloseMenu(menu, allMenusAreClosing); + } + } + + @Override + public boolean flagActionItems() { + return false; + } + + private class MenuAdapter extends BaseAdapter { + private MenuBuilder mAdapterMenu; + + public MenuAdapter(MenuBuilder menu) { + mAdapterMenu = menu; + } + + public int getCount() { + ArrayList<MenuItemImpl> items = mOverflowOnly ? + mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems(); + return items.size(); + } + + public MenuItemImpl getItem(int position) { + ArrayList<MenuItemImpl> items = mOverflowOnly ? + mAdapterMenu.getNonActionItems() : mAdapterMenu.getVisibleItems(); + return items.get(position); + } + + public long getItemId(int position) { + // Since a menu item's ID is optional, we'll use the position as an + // ID for the item in the AdapterView + return position; + } + + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = mInflater.inflate(ITEM_LAYOUT, parent, false); + } + + MenuView.ItemView itemView = (MenuView.ItemView) convertView; + itemView.initialize(getItem(position), 0); + return convertView; + } + } } diff --git a/core/java/com/android/internal/view/menu/MenuPresenter.java b/core/java/com/android/internal/view/menu/MenuPresenter.java new file mode 100644 index 0000000..5baf419 --- /dev/null +++ b/core/java/com/android/internal/view/menu/MenuPresenter.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view.menu; + +import android.content.Context; +import android.view.Menu; +import android.view.ViewGroup; + +/** + * A MenuPresenter is responsible for building views for a Menu object. + * It takes over some responsibility from the old style monolithic MenuBuilder class. + */ +public interface MenuPresenter { + /** + * Called by menu implementation to notify another component of open/close events. + */ + public interface Callback { + /** + * Called when a menu is closing. + * @param menu + * @param allMenusAreClosing + */ + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing); + + /** + * Called when a submenu opens. Useful for notifying the application + * of menu state so that it does not attempt to hide the action bar + * while a submenu is open or similar. + * + * @param subMenu Submenu currently being opened + * @return true if the Callback will handle presenting the submenu, false if + * the presenter should attempt to do so. + */ + public boolean onOpenSubMenu(MenuBuilder subMenu); + } + + /** + * Initialize this presenter for the given context and menu. + * This method is called by MenuBuilder when a presenter is + * added. See {@link MenuBuilder#addMenuPresenter(MenuPresenter)} + * + * @param context Context for this presenter; used for view creation and resource management + * @param menu Menu to host + */ + public void initForMenu(Context context, MenuBuilder menu); + + /** + * Retrieve a MenuView to display the menu specified in + * {@link #initForMenu(Context, Menu)}. + * + * @param root Intended parent of the MenuView. + * @return A freshly created MenuView. + */ + public MenuView getMenuView(ViewGroup root); + + /** + * Update the menu UI in response to a change. Called by + * MenuBuilder during the normal course of operation. + * + * @param cleared true if the menu was entirely cleared + */ + public void updateMenuView(boolean cleared); + + /** + * Set a callback object that will be notified of menu events + * related to this specific presentation. + * @param cb Callback that will be notified of future events + */ + public void setCallback(Callback cb); + + /** + * Called by Menu implementations to indicate that a submenu item + * has been selected. An active Callback should be notified, and + * if applicable the presenter should present the submenu. + * + * @param subMenu SubMenu being opened + * @return true if the the event was handled, false otherwise. + */ + public boolean onSubMenuSelected(SubMenuBuilder subMenu); + + /** + * Called by Menu implementations to indicate that a menu or submenu is + * closing. Presenter implementations should close the representation + * of the menu indicated as necessary and notify a registered callback. + * + * @param menu Menu or submenu that is closing. + * @param allMenusAreClosing True if all associated menus are closing. + */ + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing); + + /** + * Called by Menu implementations to flag items that will be shown as actions. + * @return true if this presenter changed the action status of any items. + */ + public boolean flagActionItems(); +} diff --git a/core/java/com/android/internal/view/menu/MenuView.java b/core/java/com/android/internal/view/menu/MenuView.java index 5090400..407caae 100644 --- a/core/java/com/android/internal/view/menu/MenuView.java +++ b/core/java/com/android/internal/view/menu/MenuView.java @@ -22,7 +22,7 @@ import com.android.internal.view.menu.MenuItemImpl; import android.graphics.drawable.Drawable; /** - * Minimal interface for a menu view. {@link #initialize(MenuBuilder, int)} must be called for the + * Minimal interface for a menu view. {@link #initialize(MenuBuilder)} must be called for the * menu to be functional. * * @hide @@ -33,18 +33,8 @@ public interface MenuView { * view is inflated. * * @param menu The menu that this MenuView should display. - * @param menuType The type of this menu, one of - * {@link MenuBuilder#TYPE_ICON}, {@link MenuBuilder#TYPE_EXPANDED}, - * {@link MenuBuilder#TYPE_DIALOG}). */ - public void initialize(MenuBuilder menu, int menuType); - - /** - * Forces the menu view to update its view to reflect the new state of the menu. - * - * @param cleared Whether the menu was cleared or just modified. - */ - public void updateChildren(boolean cleared); + public void initialize(MenuBuilder menu); /** * Returns the default animations to be used for this menu when entering/exiting. diff --git a/core/java/com/android/internal/view/menu/SubMenuBuilder.java b/core/java/com/android/internal/view/menu/SubMenuBuilder.java index af1b996..ad773ee 100644 --- a/core/java/com/android/internal/view/menu/SubMenuBuilder.java +++ b/core/java/com/android/internal/view/menu/SubMenuBuilder.java @@ -67,11 +67,6 @@ public class SubMenuBuilder extends MenuBuilder implements SubMenu { } @Override - public Callback getCallback() { - return mParentMenu.getCallback(); - } - - @Override public void setCallback(Callback callback) { mParentMenu.setCallback(callback); } @@ -110,5 +105,4 @@ public class SubMenuBuilder extends MenuBuilder implements SubMenu { public SubMenu setHeaderView(View view) { return (SubMenu) super.setHeaderViewInt(view); } - } diff --git a/core/java/com/android/internal/widget/AbsActionBarView.java b/core/java/com/android/internal/widget/AbsActionBarView.java new file mode 100644 index 0000000..788883b --- /dev/null +++ b/core/java/com/android/internal/widget/AbsActionBarView.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget; + +import com.android.internal.view.menu.ActionMenuPresenter; +import com.android.internal.view.menu.ActionMenuView; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; + +public abstract class AbsActionBarView extends ViewGroup { + protected ActionMenuView mMenuView; + protected ActionMenuPresenter mMenuPresenter; + protected ActionBarContainer mSplitView; + + protected Animator mVisibilityAnim; + protected final VisibilityAnimListener mVisAnimListener = new VisibilityAnimListener(); + + private static final TimeInterpolator sAlphaInterpolator = new DecelerateInterpolator(); + + private static final int FADE_DURATION = 200; + + public AbsActionBarView(Context context) { + super(context); + } + + public AbsActionBarView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AbsActionBarView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setSplitView(ActionBarContainer splitView) { + mSplitView = splitView; + } + + public void animateToVisibility(int visibility) { + if (mVisibilityAnim != null) { + mVisibilityAnim.cancel(); + } + if (visibility == VISIBLE) { + if (getVisibility() != VISIBLE) { + setAlpha(0); + if (mSplitView != null && mMenuView != null) { + mMenuView.setAlpha(0); + } + } + ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 1); + anim.setDuration(FADE_DURATION); + anim.setInterpolator(sAlphaInterpolator); + if (mSplitView != null && mMenuView != null) { + AnimatorSet set = new AnimatorSet(); + ObjectAnimator splitAnim = ObjectAnimator.ofFloat(mMenuView, "alpha", 1); + splitAnim.setDuration(FADE_DURATION); + set.addListener(mVisAnimListener.withFinalVisibility(visibility)); + set.play(anim).with(splitAnim); + set.start(); + } else { + anim.addListener(mVisAnimListener.withFinalVisibility(visibility)); + anim.start(); + } + } else { + ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 0); + anim.setDuration(FADE_DURATION); + anim.setInterpolator(sAlphaInterpolator); + if (mSplitView != null && mMenuView != null) { + AnimatorSet set = new AnimatorSet(); + ObjectAnimator splitAnim = ObjectAnimator.ofFloat(mMenuView, "alpha", 0); + splitAnim.setDuration(FADE_DURATION); + set.addListener(mVisAnimListener.withFinalVisibility(visibility)); + set.play(anim).with(splitAnim); + set.start(); + } else { + anim.addListener(mVisAnimListener.withFinalVisibility(visibility)); + anim.start(); + } + } + } + + @Override + public void setVisibility(int visibility) { + if (mVisibilityAnim != null) { + mVisibilityAnim.end(); + } + super.setVisibility(visibility); + } + + public boolean showOverflowMenu() { + if (mMenuPresenter != null) { + return mMenuPresenter.showOverflowMenu(); + } + return false; + } + + public void postShowOverflowMenu() { + post(new Runnable() { + public void run() { + showOverflowMenu(); + } + }); + } + + public boolean hideOverflowMenu() { + if (mMenuPresenter != null) { + return mMenuPresenter.hideOverflowMenu(); + } + return false; + } + + public boolean isOverflowMenuShowing() { + if (mMenuPresenter != null) { + return mMenuPresenter.isOverflowMenuShowing(); + } + return false; + } + + public boolean isOverflowReserved() { + return mMenuPresenter != null && mMenuPresenter.isOverflowReserved(); + } + + public void dismissPopupMenus() { + if (mMenuPresenter != null) { + mMenuPresenter.dismissPopupMenus(); + } + } + + protected int measureChildView(View child, int availableWidth, int childSpecHeight, + int spacing) { + child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), + childSpecHeight); + + availableWidth -= child.getMeasuredWidth(); + availableWidth -= spacing; + + return Math.max(0, availableWidth); + } + + protected int positionChild(View child, int x, int y, int contentHeight) { + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + int childTop = y + (contentHeight - childHeight) / 2; + + child.layout(x, childTop, x + childWidth, childTop + childHeight); + + return childWidth; + } + + protected int positionChildInverse(View child, int x, int y, int contentHeight) { + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + int childTop = y + (contentHeight - childHeight) / 2; + + child.layout(x - childWidth, childTop, x, childTop + childHeight); + + return childWidth; + } + + protected class VisibilityAnimListener implements Animator.AnimatorListener { + private boolean mCanceled = false; + private int mFinalVisibility; + + public VisibilityAnimListener withFinalVisibility(int visibility) { + mFinalVisibility = visibility; + return this; + } + + @Override + public void onAnimationStart(Animator animation) { + setVisibility(VISIBLE); + mVisibilityAnim = animation; + mCanceled = false; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCanceled) return; + + mVisibilityAnim = null; + setVisibility(mFinalVisibility); + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + } +} diff --git a/core/java/com/android/internal/widget/ActionBarContainer.java b/core/java/com/android/internal/widget/ActionBarContainer.java index c9b0ec9..c18565d 100644 --- a/core/java/com/android/internal/widget/ActionBarContainer.java +++ b/core/java/com/android/internal/widget/ActionBarContainer.java @@ -19,7 +19,9 @@ package com.android.internal.widget; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import android.view.ActionMode; import android.view.MotionEvent; +import android.view.View; import android.widget.FrameLayout; /** @@ -29,6 +31,7 @@ import android.widget.FrameLayout; */ public class ActionBarContainer extends FrameLayout { private boolean mIsTransitioning; + private View mTabContainer; public ActionBarContainer(Context context) { this(context, null); @@ -65,6 +68,50 @@ public class ActionBarContainer extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent ev) { super.onTouchEvent(ev); + + // An action bar always eats touch events. return true; } + + public void setTabContainer(View tabView) { + if (mTabContainer != null) { + removeView(mTabContainer); + } + mTabContainer = tabView; + addView(tabView); + } + + public View getTabContainer() { + return mTabContainer; + } + + @Override + public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) { + // No starting an action mode for an action bar child! (Where would it go?) + return null; + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mTabContainer != null && mTabContainer.getVisibility() != GONE) { + final int mode = MeasureSpec.getMode(heightMeasureSpec); + if (mode == MeasureSpec.AT_MOST) { + final int measuredHeight = getMeasuredHeight(); + final int maxHeight = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), + Math.min(measuredHeight + mTabContainer.getMeasuredHeight(), maxHeight)); + } + } + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + if (mTabContainer != null && mTabContainer.getVisibility() != GONE) { + final int containerHeight = getMeasuredHeight(); + mTabContainer.layout(l, containerHeight - mTabContainer.getMeasuredHeight(), + r, containerHeight); + } + } } diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java index f762265..c82323e 100644 --- a/core/java/com/android/internal/widget/ActionBarContextView.java +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -16,6 +16,7 @@ package com.android.internal.widget; import com.android.internal.R; +import com.android.internal.view.menu.ActionMenuPresenter; import com.android.internal.view.menu.ActionMenuView; import com.android.internal.view.menu.MenuBuilder; @@ -29,7 +30,7 @@ import android.util.AttributeSet; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; import android.view.animation.DecelerateInterpolator; import android.widget.LinearLayout; import android.widget.TextView; @@ -37,7 +38,7 @@ import android.widget.TextView; /** * @hide */ -public class ActionBarContextView extends ViewGroup implements AnimatorListener { +public class ActionBarContextView extends AbsActionBarView implements AnimatorListener { private static final String TAG = "ActionBarContextView"; private int mContentHeight; @@ -52,7 +53,6 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener private TextView mSubtitleView; private int mTitleStyleRes; private int mSubtitleStyleRes; - private ActionMenuView mMenuView; private Animator mCurrentAnimation; private boolean mAnimateInOnLayout; @@ -85,12 +85,6 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener com.android.internal.R.styleable.ActionMode_height, 0); a.recycle(); } - - @Override - public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) { - // No starting an action mode for an existing action mode UI child! (Where would it go?) - return null; - } public void setHeight(int height) { mContentHeight = height; @@ -176,10 +170,25 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener }); final MenuBuilder menu = (MenuBuilder) mode.getMenu(); - mMenuView = (ActionMenuView) menu.getMenuView(MenuBuilder.TYPE_ACTION_BUTTON, this); - mMenuView.setOverflowReserved(true); - mMenuView.updateChildren(false); - addView(mMenuView); + mMenuPresenter = new ActionMenuPresenter(); + menu.addMenuPresenter(mMenuPresenter); + mMenuView = (ActionMenuView) mMenuPresenter.getMenuView(this); + + final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT); + mMenuView.setLayoutParams(layoutParams); + if (mSplitView == null) { + addView(mMenuView); + } else { + // Allow full screen width in split mode. + mMenuPresenter.setWidthLimit( + getContext().getResources().getDisplayMetrics().widthPixels, true); + // No limit to the item count; use whatever will fit. + mMenuPresenter.setItemLimit(Integer.MAX_VALUE); + // Span the whole width + layoutParams.width = LayoutParams.MATCH_PARENT; + mSplitView.addView(mMenuView); + } mAnimateInOnLayout = true; } @@ -211,34 +220,31 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener public void killMode() { finishAnimation(); removeAllViews(); + if (mSplitView != null) { + mSplitView.removeView(mMenuView); + } mCustomView = null; mMenuView = null; mAnimateInOnLayout = false; } public boolean showOverflowMenu() { - if (mMenuView != null) { - return mMenuView.showOverflowMenu(); + if (mMenuPresenter != null) { + return mMenuPresenter.showOverflowMenu(); } return false; } - public void openOverflowMenu() { - if (mMenuView != null) { - mMenuView.openOverflowMenu(); - } - } - public boolean hideOverflowMenu() { - if (mMenuView != null) { - return mMenuView.hideOverflowMenu(); + if (mMenuPresenter != null) { + return mMenuPresenter.hideOverflowMenu(); } return false; } public boolean isOverflowMenuShowing() { - if (mMenuView != null) { - return mMenuView.isOverflowMenuShowing(); + if (mMenuPresenter != null) { + return mMenuPresenter.isOverflowMenuShowing(); } return false; } @@ -346,7 +352,7 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener private Animator makeOutAnimation() { ObjectAnimator buttonAnimator = ObjectAnimator.ofFloat(mClose, "translationX", - 0, -mClose.getWidth()); + -mClose.getWidth()); buttonAnimator.setDuration(200); buttonAnimator.addListener(this); buttonAnimator.setInterpolator(new DecelerateInterpolator()); @@ -360,7 +366,7 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener for (int i = 0; i < 0; i++) { View child = mMenuView.getChildAt(i); child.setScaleY(0); - ObjectAnimator a = ObjectAnimator.ofFloat(child, "scaleY", 1, 0); + ObjectAnimator a = ObjectAnimator.ofFloat(child, "scaleY", 0); a.setDuration(100); a.setStartDelay(i * 70); b.with(a); @@ -387,7 +393,7 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener mAnimateInOnLayout = false; } } - + if (mTitleLayout != null && mCustomView == null) { x += positionChild(mTitleLayout, x, y, contentHeight); } @@ -403,36 +409,6 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener } } - private int measureChildView(View child, int availableWidth, int childSpecHeight, int spacing) { - child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), - childSpecHeight); - - availableWidth -= child.getMeasuredWidth(); - availableWidth -= spacing; - - return Math.max(0, availableWidth); - } - - private int positionChild(View child, int x, int y, int contentHeight) { - int childWidth = child.getMeasuredWidth(); - int childHeight = child.getMeasuredHeight(); - int childTop = y + (contentHeight - childHeight) / 2; - - child.layout(x, childTop, x + childWidth, childTop + childHeight); - - return childWidth; - } - - private int positionChildInverse(View child, int x, int y, int contentHeight) { - int childWidth = child.getMeasuredWidth(); - int childHeight = child.getMeasuredHeight(); - int childTop = y + (contentHeight - childHeight) / 2; - - child.layout(x - childWidth, childTop, x, childTop + childHeight); - - return childWidth; - } - @Override public void onAnimationStart(Animator animation) { } @@ -452,4 +428,9 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener @Override public void onAnimationRepeat(Animator animation) { } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } } diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index 891557d..f1887eb 100644 --- a/core/java/com/android/internal/widget/ActionBarView.java +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -18,8 +18,10 @@ package com.android.internal.widget; import com.android.internal.R; import com.android.internal.view.menu.ActionMenuItem; +import com.android.internal.view.menu.ActionMenuPresenter; import com.android.internal.view.menu.ActionMenuView; import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.view.menu.MenuPresenter; import android.app.ActionBar; import android.app.ActionBar.OnNavigationListener; @@ -28,15 +30,14 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; import android.util.AttributeSet; +import android.util.DisplayMetrics; import android.util.Log; -import android.view.ActionMode; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; @@ -57,7 +58,7 @@ import android.widget.TextView; /** * @hide */ -public class ActionBarView extends ViewGroup { +public class ActionBarView extends AbsActionBarView { private static final String TAG = "ActionBarView"; /** @@ -85,7 +86,6 @@ public class ActionBarView extends ViewGroup { private CharSequence mSubtitle; private Drawable mIcon; private Drawable mLogo; - private Drawable mDivider; private View mHomeLayout; private View mHomeAsUpView; @@ -96,7 +96,7 @@ public class ActionBarView extends ViewGroup { private Spinner mSpinner; private LinearLayout mListNavLayout; private HorizontalScrollView mTabScrollView; - private LinearLayout mTabLayout; + private ViewGroup mTabLayout; private View mCustomNavView; private ProgressBar mProgressView; private ProgressBar mIndeterminateProgressView; @@ -109,11 +109,11 @@ public class ActionBarView extends ViewGroup { private int mProgressStyle; private int mIndeterminateProgressStyle; - private boolean mShowMenu; + private boolean mSplitActionBar; private boolean mUserTitle; + private boolean mIncludeTabs; private MenuBuilder mOptionsMenu; - private ActionMenuView mMenuView; private ActionBarContextView mContextView; @@ -199,6 +199,8 @@ public class ActionBarView extends ViewGroup { mProgressBarPadding = a.getDimensionPixelOffset(R.styleable.ActionBar_progressBarPadding, 0); mItemPadding = a.getDimensionPixelOffset(R.styleable.ActionBar_itemPadding, 0); + mIncludeTabs = a.getBoolean(R.styleable.ActionBar_embeddedTabs, true); + setDisplayOptions(a.getInt(R.styleable.ActionBar_displayOptions, DISPLAY_DEFAULT)); final int customNavId = a.getResourceId(R.styleable.ActionBar_customNavigationLayout, 0); @@ -210,8 +212,6 @@ public class ActionBarView extends ViewGroup { mContentHeight = a.getLayoutDimension(R.styleable.ActionBar_height, 0); - mDivider = a.getDrawable(R.styleable.ActionBar_divider); - a.recycle(); mLogoNavItem = new ActionMenuItem(context, 0, android.R.id.home, 0, 0, mTitle); @@ -228,6 +228,11 @@ public class ActionBarView extends ViewGroup { mHomeLayout.setFocusable(true); } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + public void initProgress() { mProgressView = new ProgressBar(mContext, null, 0, mProgressStyle); mProgressView.setId(R.id.progress_horizontal); @@ -242,77 +247,74 @@ public class ActionBarView extends ViewGroup { addView(mIndeterminateProgressView); } - @Override - public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) { - // No starting an action mode for an action bar child! (Where would it go?) - return null; + public void setSplitActionBar(boolean splitActionBar) { + if (mSplitActionBar != splitActionBar) { + if (mMenuView != null) { + if (splitActionBar) { + removeView(mMenuView); + if (mSplitView != null) { + mSplitView.addView(mMenuView); + } + } else { + addView(mMenuView); + } + } + mSplitActionBar = splitActionBar; + } + } + + public boolean isSplitActionBar() { + return mSplitActionBar; + } + + public boolean hasEmbeddedTabs() { + return mIncludeTabs; + } + + public void setExternalTabLayout(ViewGroup tabLayout) { + mTabLayout = tabLayout; } public void setCallback(OnNavigationListener callback) { mCallback = callback; } - public void setMenu(Menu menu) { + public void setMenu(Menu menu, MenuPresenter.Callback cb) { if (menu == mOptionsMenu) return; + if (mOptionsMenu != null) { + mOptionsMenu.removeMenuPresenter(mMenuPresenter); + } + MenuBuilder builder = (MenuBuilder) menu; mOptionsMenu = builder; if (mMenuView != null) { removeView(mMenuView); } - final ActionMenuView menuView = (ActionMenuView) builder.getMenuView( - MenuBuilder.TYPE_ACTION_BUTTON, null); + if (mMenuPresenter == null) { + mMenuPresenter = new ActionMenuPresenter(); + mMenuPresenter.setCallback(cb); + builder.addMenuPresenter(mMenuPresenter); + } + final ActionMenuView menuView = (ActionMenuView) mMenuPresenter.getMenuView(this); final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); menuView.setLayoutParams(layoutParams); - addView(menuView); - mMenuView = menuView; - } - - public boolean showOverflowMenu() { - if (mMenuView != null) { - return mMenuView.showOverflowMenu(); - } - return false; - } - - public void openOverflowMenu() { - if (mMenuView != null) { - mMenuView.openOverflowMenu(); - } - } - - public void postShowOverflowMenu() { - post(new Runnable() { - public void run() { - showOverflowMenu(); - } - }); - } - - public boolean hideOverflowMenu() { - if (mMenuView != null) { - return mMenuView.hideOverflowMenu(); - } - return false; - } - - public boolean isOverflowMenuShowing() { - if (mMenuView != null) { - return mMenuView.isOverflowMenuShowing(); - } - return false; - } - - public boolean isOverflowMenuOpen() { - if (mMenuView != null) { - return mMenuView.isOverflowMenuOpen(); + if (!mSplitActionBar) { + addView(menuView); + } else { + // Allow full screen width in split mode. + mMenuPresenter.setWidthLimit( + getContext().getResources().getDisplayMetrics().widthPixels, true); + // No limit to the item count; use whatever will fit. + mMenuPresenter.setItemLimit(Integer.MAX_VALUE); + // Span the whole width + layoutParams.width = LayoutParams.MATCH_PARENT; + if (mSplitView != null) { + mSplitView.addView(menuView); + } // We'll add this later if we missed it this time. } - return false; - } - - public boolean isOverflowReserved() { - return mMenuView != null && mMenuView.isOverflowReserved(); + mMenuView = menuView; } public void setCustomNavigationView(View view) { @@ -382,13 +384,19 @@ public class ActionBarView extends ViewGroup { public void setDisplayOptions(int options) { final int flagsChanged = options ^ mDisplayOptions; mDisplayOptions = options; + + if ((flagsChanged & ActionBar.DISPLAY_DISABLE_HOME) != 0) { + final boolean disableHome = (options & ActionBar.DISPLAY_DISABLE_HOME) != 0; + mHomeLayout.setEnabled(!disableHome); + } + if ((flagsChanged & DISPLAY_RELAYOUT_MASK) != 0) { final int vis = (options & ActionBar.DISPLAY_SHOW_HOME) != 0 ? VISIBLE : GONE; mHomeLayout.setVisibility(vis); if ((flagsChanged & ActionBar.DISPLAY_HOME_AS_UP) != 0) { - mHomeAsUpView.setVisibility((options & ActionBar.DISPLAY_HOME_AS_UP) != 0 - ? VISIBLE : GONE); + final boolean isUp = (options & ActionBar.DISPLAY_HOME_AS_UP) != 0; + mHomeAsUpView.setVisibility(isUp ? VISIBLE : GONE); } if ((flagsChanged & ActionBar.DISPLAY_USE_LOGO) != 0) { @@ -416,6 +424,59 @@ public class ActionBarView extends ViewGroup { } else { invalidate(); } + + // Make sure the home button has an accurate content description for accessibility. + if ((options & ActionBar.DISPLAY_DISABLE_HOME) != 0) { + mHomeLayout.setContentDescription(null); + } else if ((options & ActionBar.DISPLAY_HOME_AS_UP) != 0) { + mHomeLayout.setContentDescription(mContext.getResources().getText( + R.string.action_bar_up_description)); + } else { + mHomeLayout.setContentDescription(mContext.getResources().getText( + R.string.action_bar_home_description)); + } + } + + public void setIcon(Drawable icon) { + mIcon = icon; + if (icon != null && + ((mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) == 0 || mLogo == null)) { + mIconView.setImageDrawable(icon); + } + } + + public void setIcon(int resId) { + setIcon(mContext.getResources().getDrawableForDensity(resId, getPreferredIconDensity())); + } + + public void setLogo(Drawable logo) { + mLogo = logo; + if (logo != null && (mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) != 0) { + mIconView.setImageDrawable(logo); + } + } + + public void setLogo(int resId) { + mContext.getResources().getDrawable(resId); + } + + /** + * @return Drawable density to load that will best fit the available height. + */ + private int getPreferredIconDensity() { + final Resources res = mContext.getResources(); + final int availableHeight = getLayoutParams().height - + mIconView.getPaddingTop() - mIconView.getPaddingBottom(); + int iconSize = res.getDimensionPixelSize(android.R.dimen.app_icon_size); + + if (iconSize * DisplayMetrics.DENSITY_LOW >= availableHeight) { + return DisplayMetrics.DENSITY_LOW; + } else if (iconSize * DisplayMetrics.DENSITY_MEDIUM >= availableHeight) { + return DisplayMetrics.DENSITY_MEDIUM; + } else if (iconSize * DisplayMetrics.DENSITY_HIGH >= availableHeight) { + return DisplayMetrics.DENSITY_HIGH; + } + return DisplayMetrics.DENSITY_XHIGH; } public void setNavigationMode(int mode) { @@ -428,7 +489,7 @@ public class ActionBarView extends ViewGroup { } break; case ActionBar.NAVIGATION_MODE_TABS: - if (mTabLayout != null) { + if (mTabScrollView != null) { removeView(mTabScrollView); } } @@ -453,7 +514,9 @@ public class ActionBarView extends ViewGroup { break; case ActionBar.NAVIGATION_MODE_TABS: ensureTabsExist(); - addView(mTabScrollView); + if (mTabScrollView != null) { + addView(mTabScrollView); + } break; } mNavigationMode = mode; @@ -462,15 +525,24 @@ public class ActionBarView extends ViewGroup { } private void ensureTabsExist() { + if (!mIncludeTabs) return; + if (mTabScrollView == null) { mTabScrollView = new HorizontalScrollView(getContext()); mTabScrollView.setHorizontalFadingEdgeEnabled(true); - mTabLayout = new LinearLayout(getContext(), null, - com.android.internal.R.attr.actionBarTabBarStyle); + mTabLayout = createTabContainer(); mTabScrollView.addView(mTabLayout); } } + public ViewGroup createTabContainer() { + ViewGroup result = new LinearLayout(getContext(), null, + com.android.internal.R.attr.actionBarTabBarStyle); + result.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, + mContentHeight)); + return result; + } + public void setDropdownAdapter(SpinnerAdapter adapter) { mSpinnerAdapter = adapter; if (mSpinner != null) { @@ -531,6 +603,10 @@ public class ActionBarView extends ViewGroup { } } + public void updateTab(int position) { + ((TabView) mTabLayout.getChildAt(position)).update(); + } + public void removeTabAt(int position) { if (mTabLayout != null) { mTabLayout.removeViewAt(position); @@ -641,7 +717,7 @@ public class ActionBarView extends ViewGroup { leftOfCenter = Math.max(0, availableWidth - homeWidth); } - if (mMenuView != null) { + if (mMenuView != null && mMenuView.getParent() == this) { availableWidth = measureChildView(mMenuView, availableWidth, childSpecHeight, 0); rightOfCenter = Math.max(0, rightOfCenter - mMenuView.getMeasuredWidth()); @@ -760,16 +836,6 @@ public class ActionBarView extends ViewGroup { } } - private int measureChildView(View child, int availableWidth, int childSpecHeight, int spacing) { - child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), - childSpecHeight); - - availableWidth -= child.getMeasuredWidth(); - availableWidth -= spacing; - - return Math.max(0, availableWidth); - } - @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int x = getPaddingLeft(); @@ -804,7 +870,7 @@ public class ActionBarView extends ViewGroup { } int menuLeft = r - l - getPaddingRight(); - if (mMenuView != null) { + if (mMenuView != null && mMenuView.getParent() == this) { positionChildInverse(mMenuView, menuLeft, y, contentHeight); menuLeft -= mMenuView.getMeasuredWidth(); } @@ -882,68 +948,78 @@ public class ActionBarView extends ViewGroup { } } - private int positionChild(View child, int x, int y, int contentHeight) { - int childWidth = child.getMeasuredWidth(); - int childHeight = child.getMeasuredHeight(); - int childTop = y + (contentHeight - childHeight) / 2; - - child.layout(x, childTop, x + childWidth, childTop + childHeight); - - return childWidth; - } - - private int positionChildInverse(View child, int x, int y, int contentHeight) { - int childWidth = child.getMeasuredWidth(); - int childHeight = child.getMeasuredHeight(); - int childTop = y + (contentHeight - childHeight) / 2; - - child.layout(x - childWidth, childTop, x, childTop + childHeight); - - return childWidth; - } - private static class TabView extends LinearLayout { private ActionBar.Tab mTab; + private TextView mTextView; + private ImageView mIconView; + private View mCustomView; public TabView(Context context, ActionBar.Tab tab) { super(context, null, com.android.internal.R.attr.actionBarTabStyle); mTab = tab; + update(); + + setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT, 1)); + } + + public void update() { + final ActionBar.Tab tab = mTab; final View custom = tab.getCustomView(); if (custom != null) { addView(custom); + mCustomView = custom; + if (mTextView != null) mTextView.setVisibility(GONE); + if (mIconView != null) { + mIconView.setVisibility(GONE); + mIconView.setImageDrawable(null); + } } else { - // TODO Style tabs based on the theme + if (mCustomView != null) { + removeView(mCustomView); + mCustomView = null; + } final Drawable icon = tab.getIcon(); final CharSequence text = tab.getText(); if (icon != null) { - ImageView iconView = new ImageView(context); - iconView.setImageDrawable(icon); - LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - lp.gravity = Gravity.CENTER_VERTICAL; - iconView.setLayoutParams(lp); - addView(iconView); + if (mIconView == null) { + ImageView iconView = new ImageView(getContext()); + LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER_VERTICAL; + iconView.setLayoutParams(lp); + addView(iconView, 0); + mIconView = iconView; + } + mIconView.setImageDrawable(icon); + mIconView.setVisibility(VISIBLE); + } else if (mIconView != null) { + mIconView.setVisibility(GONE); + mIconView.setImageDrawable(null); } if (text != null) { - TextView textView = new TextView(context, null, - com.android.internal.R.attr.actionBarTabTextStyle); - textView.setText(text); - textView.setSingleLine(); - textView.setEllipsize(TruncateAt.END); - LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - lp.gravity = Gravity.CENTER_VERTICAL; - textView.setLayoutParams(lp); - addView(textView); + if (mTextView == null) { + TextView textView = new TextView(getContext(), null, + com.android.internal.R.attr.actionBarTabTextStyle); + textView.setSingleLine(); + textView.setEllipsize(TruncateAt.END); + LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER_VERTICAL; + textView.setLayoutParams(lp); + addView(textView); + mTextView = textView; + } + mTextView.setText(text); + mTextView.setVisibility(VISIBLE); + } else { + mTextView.setVisibility(GONE); } } - - setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.MATCH_PARENT, 1)); } public ActionBar.Tab getTab() { @@ -982,21 +1058,6 @@ public class ActionBarView extends ViewGroup { } @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - // Make sure we reload positioning elements that may change with configuration. - Resources res = getContext().getResources(); - final int imagePadding = res.getDimensionPixelSize( - com.android.internal.R.dimen.action_bar_home_image_padding); - final int upMargin = res.getDimensionPixelSize( - com.android.internal.R.dimen.action_bar_home_up_margin); - mIconView.setPadding(imagePadding, getPaddingTop(), imagePadding, getPaddingBottom()); - ((LayoutParams) mUpView.getLayoutParams()).rightMargin = upMargin; - mUpView.requestLayout(); - } - - @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { measureChildWithMargins(mUpView, widthMeasureSpec, 0, heightMeasureSpec, 0); final LayoutParams upLp = (LayoutParams) mUpView.getLayoutParams(); diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java index 9f9f020..0d32d4b 100644 --- a/core/java/com/android/internal/widget/EditableInputConnection.java +++ b/core/java/com/android/internal/widget/EditableInputConnection.java @@ -17,6 +17,7 @@ package com.android.internal.widget; import android.os.Bundle; +import android.os.IBinder; import android.text.Editable; import android.text.method.KeyListener; import android.util.Log; diff --git a/core/java/com/android/internal/widget/IRemoteViewsFactory.aidl b/core/java/com/android/internal/widget/IRemoteViewsFactory.aidl index 5857acb..18076c4 100644 --- a/core/java/com/android/internal/widget/IRemoteViewsFactory.aidl +++ b/core/java/com/android/internal/widget/IRemoteViewsFactory.aidl @@ -22,7 +22,7 @@ import android.widget.RemoteViews; /** {@hide} */ interface IRemoteViewsFactory { void onDataSetChanged(); - void onDestroy(in Intent intent); + oneway void onDestroy(in Intent intent); int getCount(); RemoteViews getViewAt(int position); RemoteViews getLoadingView(); diff --git a/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java b/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java index 65973b6..3070e3e 100644 --- a/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java +++ b/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java @@ -29,7 +29,7 @@ import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; -import android.view.ViewRoot; +import android.view.ViewAncestor; import com.android.internal.R; public class PasswordEntryKeyboardHelper implements OnKeyboardActionListener { @@ -150,7 +150,7 @@ public class PasswordEntryKeyboardHelper implements OnKeyboardActionListener { KeyEvent event = events[i]; event = KeyEvent.changeFlags(event, event.getFlags() | KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); - handler.sendMessage(handler.obtainMessage(ViewRoot.DISPATCH_KEY, event)); + handler.sendMessage(handler.obtainMessage(ViewAncestor.DISPATCH_KEY, event)); } } } @@ -158,11 +158,11 @@ public class PasswordEntryKeyboardHelper implements OnKeyboardActionListener { public void sendDownUpKeyEvents(int keyEventCode) { long eventTime = SystemClock.uptimeMillis(); Handler handler = mTargetView.getHandler(); - handler.sendMessage(handler.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + handler.sendMessage(handler.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE))); - handler.sendMessage(handler.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + handler.sendMessage(handler.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE))); diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java index 076a1cb..d789584 100644 --- a/core/java/com/android/internal/widget/PointerLocationView.java +++ b/core/java/com/android/internal/widget/PointerLocationView.java @@ -320,7 +320,8 @@ public class PointerLocationView extends View { } } - private void logPointerCoords(int action, int index, MotionEvent.PointerCoords coords, int id) { + private void logPointerCoords(int action, int index, MotionEvent.PointerCoords coords, int id, + int toolType, int buttonState) { final String prefix; switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: @@ -357,6 +358,12 @@ public class PointerLocationView extends View { case MotionEvent.ACTION_HOVER_MOVE: prefix = "HOVER MOVE"; break; + case MotionEvent.ACTION_HOVER_ENTER: + prefix = "HOVER ENTER"; + break; + case MotionEvent.ACTION_HOVER_EXIT: + prefix = "HOVER EXIT"; + break; case MotionEvent.ACTION_SCROLL: prefix = "SCROLL"; break; @@ -380,26 +387,16 @@ public class PointerLocationView extends View { .append("deg") .append(" VScroll=").append(coords.getAxisValue(MotionEvent.AXIS_VSCROLL), 1) .append(" HScroll=").append(coords.getAxisValue(MotionEvent.AXIS_HSCROLL), 1) + .append(" ToolType=").append(MotionEvent.toolTypeToString(toolType)) + .append(" ButtonState=").append(MotionEvent.buttonStateToString(buttonState)) .toString()); } public void addPointerEvent(MotionEvent event) { synchronized (mPointers) { - int action = event.getAction(); - - //Log.i(TAG, "Motion: action=0x" + Integer.toHexString(action) - // + " pointers=" + event.getPointerCount()); - + final int action = event.getAction(); int NP = mPointers.size(); - - //mRect.set(0, 0, getWidth(), mHeaderBottom+1); - //invalidate(mRect); - //if (mCurDown) { - // mRect.set(mCurX-mCurWidth-3, mCurY-mCurWidth-3, - // mCurX+mCurWidth+3, mCurY+mCurWidth+3); - //} else { - // mRect.setEmpty(); - //} + if (action == MotionEvent.ACTION_DOWN || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) @@ -450,7 +447,8 @@ public class PointerLocationView extends View { final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords; event.getHistoricalPointerCoords(i, historyPos, coords); if (mPrintCoords) { - logPointerCoords(action, i, coords, id); + logPointerCoords(action, i, coords, id, + event.getToolType(i), event.getButtonState()); } if (ps != null) { ps.addTrace(coords.x, coords.y); @@ -463,7 +461,8 @@ public class PointerLocationView extends View { final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords; event.getPointerCoords(i, coords); if (mPrintCoords) { - logPointerCoords(action, i, coords, id); + logPointerCoords(action, i, coords, id, + event.getToolType(i), event.getButtonState()); } if (ps != null) { ps.addTrace(coords.x, coords.y); @@ -494,12 +493,7 @@ public class PointerLocationView extends View { ps.addTrace(Float.NaN, Float.NaN); } } - - //if (mCurDown) { - // mRect.union(mCurX-mCurWidth-3, mCurY-mCurWidth-3, - // mCurX+mCurWidth+3, mCurY+mCurWidth+3); - //} - //invalidate(mRect); + postInvalidate(); } } diff --git a/core/java/com/google/android/mms/pdu/EncodedStringValue.java b/core/java/com/google/android/mms/pdu/EncodedStringValue.java index a27962d..9495c1c 100644 --- a/core/java/com/google/android/mms/pdu/EncodedStringValue.java +++ b/core/java/com/google/android/mms/pdu/EncodedStringValue.java @@ -17,7 +17,6 @@ package com.google.android.mms.pdu; -import android.util.Config; import android.util.Log; import java.io.ByteArrayOutputStream; @@ -31,7 +30,7 @@ import java.util.ArrayList; public class EncodedStringValue implements Cloneable { private static final String TAG = "EncodedStringValue"; private static final boolean DEBUG = false; - private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean LOCAL_LOGV = false; /** * The Char-set value. diff --git a/core/java/com/google/android/mms/pdu/PduParser.java b/core/java/com/google/android/mms/pdu/PduParser.java index 3f185aa..f7f71ed 100755 --- a/core/java/com/google/android/mms/pdu/PduParser.java +++ b/core/java/com/google/android/mms/pdu/PduParser.java @@ -20,7 +20,6 @@ package com.google.android.mms.pdu; import com.google.android.mms.ContentType; import com.google.android.mms.InvalidHeaderValueException; -import android.util.Config; import android.util.Log; import java.io.ByteArrayInputStream; @@ -86,7 +85,7 @@ public class PduParser { */ private static final String LOG_TAG = "PduParser"; private static final boolean DEBUG = false; - private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean LOCAL_LOGV = false; /** * Constructor. diff --git a/core/java/com/google/android/mms/pdu/PduPersister.java b/core/java/com/google/android/mms/pdu/PduPersister.java index 9fdd204..4d2d535 100644 --- a/core/java/com/google/android/mms/pdu/PduPersister.java +++ b/core/java/com/google/android/mms/pdu/PduPersister.java @@ -39,7 +39,6 @@ import android.provider.Telephony.Mms.Addr; import android.provider.Telephony.Mms.Part; import android.provider.Telephony.MmsSms.PendingMessages; import android.text.TextUtils; -import android.util.Config; import android.util.Log; import java.io.ByteArrayOutputStream; @@ -63,7 +62,7 @@ import com.google.android.mms.pdu.EncodedStringValue; public class PduPersister { private static final String TAG = "PduPersister"; private static final boolean DEBUG = false; - private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean LOCAL_LOGV = false; private static final long DUMMY_THREAD_ID = Long.MAX_VALUE; diff --git a/core/java/com/google/android/mms/util/AbstractCache.java b/core/java/com/google/android/mms/util/AbstractCache.java index 670439c..39b2abf 100644 --- a/core/java/com/google/android/mms/util/AbstractCache.java +++ b/core/java/com/google/android/mms/util/AbstractCache.java @@ -17,7 +17,6 @@ package com.google.android.mms.util; -import android.util.Config; import android.util.Log; import java.util.HashMap; @@ -25,7 +24,7 @@ import java.util.HashMap; public abstract class AbstractCache<K, V> { private static final String TAG = "AbstractCache"; private static final boolean DEBUG = false; - private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean LOCAL_LOGV = false; private static final int MAX_CACHED_ITEMS = 500; diff --git a/core/java/com/google/android/mms/util/PduCache.java b/core/java/com/google/android/mms/util/PduCache.java index 866ca1e..059af72 100644 --- a/core/java/com/google/android/mms/util/PduCache.java +++ b/core/java/com/google/android/mms/util/PduCache.java @@ -21,7 +21,6 @@ import android.content.ContentUris; import android.content.UriMatcher; import android.net.Uri; import android.provider.Telephony.Mms; -import android.util.Config; import android.util.Log; import java.util.HashMap; @@ -30,7 +29,7 @@ import java.util.HashSet; public final class PduCache extends AbstractCache<Uri, PduCacheEntry> { private static final String TAG = "PduCache"; private static final boolean DEBUG = false; - private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean LOCAL_LOGV = false; private static final int MMS_ALL = 0; private static final int MMS_ALL_ID = 1; |
