diff options
Diffstat (limited to 'core/java')
247 files changed, 13592 insertions, 5173 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/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 edfd6ef..f5849c2 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; @@ -819,18 +818,6 @@ public class Activity extends ContextThemeWrapper return mWindow != null ? mWindow.getCurrentFocus() : null; } - @Override - public int getWallpaperDesiredMinimumWidth() { - int width = super.getWallpaperDesiredMinimumWidth(); - return width <= 0 ? getWindowManager().getDefaultDisplay().getWidth() : width; - } - - @Override - public int getWallpaperDesiredMinimumHeight() { - int height = super.getWallpaperDesiredMinimumHeight(); - return height <= 0 ? getWindowManager().getDefaultDisplay().getHeight() : height; - } - /** * Called when the activity is starting. This is where most initialization * should go: calling {@link #setContentView(int)} to inflate the @@ -1438,6 +1425,10 @@ public class Activity extends ContextThemeWrapper } /** + * @deprecated Use the new {@link Fragment} API + * {@link Fragment#setRetainInstance(boolean)} instead; this is also + * available on older platforms through the Android compatibility package. + * * Retrieve the non-configuration instance data that was previously * returned by {@link #onRetainNonConfigurationInstance()}. This will * be available from the initial {@link #onCreate} and @@ -1454,12 +1445,17 @@ public class Activity extends ContextThemeWrapper * @return Returns the object previously returned by * {@link #onRetainNonConfigurationInstance()}. */ + @Deprecated public Object getLastNonConfigurationInstance() { return mLastNonConfigurationInstances != null ? mLastNonConfigurationInstances.activity : null; } /** + * @deprecated Use the new {@link Fragment} API + * {@link Fragment#setRetainInstance(boolean)} instead; this is also + * available on older platforms through the Android compatibility package. + * * Called by the system, as part of destroying an * activity due to a configuration change, when it is known that a new * instance will immediately be created for the new configuration. You @@ -1675,6 +1671,10 @@ public class Activity extends ContextThemeWrapper } /** + * @deprecated Use the new {@link android.content.CursorLoader} class with + * {@link LoaderManager} instead; this is also + * available on older platforms through the Android compatibility package. + * * This method allows the activity to take care of managing the given * {@link Cursor}'s lifecycle for you based on the activity's lifecycle. * That is, when the activity is stopped it will automatically call @@ -1690,8 +1690,6 @@ public class Activity extends ContextThemeWrapper * * @see #managedQuery(android.net.Uri , String[], String, String[], String) * @see #stopManagingCursor - * - * @deprecated Use {@link CursorLoader} instead. */ @Deprecated public void startManagingCursor(Cursor c) { @@ -1701,6 +1699,10 @@ public class Activity extends ContextThemeWrapper } /** + * @deprecated Use the new {@link android.content.CursorLoader} class with + * {@link LoaderManager} instead; this is also + * available on older platforms through the Android compatibility package. + * * Given a Cursor that was previously given to * {@link #startManagingCursor}, stop the activity's management of that * cursor. @@ -1708,8 +1710,6 @@ public class Activity extends ContextThemeWrapper * @param c The Cursor that was being managed. * * @see #startManagingCursor - * - * @deprecated Use {@link CursorLoader} instead. */ @Deprecated public void stopManagingCursor(Cursor c) { @@ -2715,6 +2715,10 @@ public class Activity extends ContextThemeWrapper } /** + * @deprecated Use the new {@link DialogFragment} class with + * {@link FragmentManager} instead; this is also + * available on older platforms through the Android compatibility package. + * * Callback for creating dialogs that are managed (saved and restored) for you * by the activity. The default implementation calls through to * {@link #onCreateDialog(int)} for compatibility. @@ -2743,6 +2747,7 @@ public class Activity extends ContextThemeWrapper * @see #dismissDialog(int) * @see #removeDialog(int) */ + @Deprecated protected Dialog onCreateDialog(int id, Bundle args) { return onCreateDialog(id); } @@ -2757,6 +2762,10 @@ public class Activity extends ContextThemeWrapper } /** + * @deprecated Use the new {@link DialogFragment} class with + * {@link FragmentManager} instead; this is also + * available on older platforms through the Android compatibility package. + * * Provides an opportunity to prepare a managed dialog before it is being * shown. The default implementation calls through to * {@link #onPrepareDialog(int, Dialog)} for compatibility. @@ -2776,20 +2785,30 @@ public class Activity extends ContextThemeWrapper * @see #dismissDialog(int) * @see #removeDialog(int) */ + @Deprecated protected void onPrepareDialog(int id, Dialog dialog, Bundle args) { onPrepareDialog(id, dialog); } /** + * @deprecated Use the new {@link DialogFragment} class with + * {@link FragmentManager} instead; this is also + * available on older platforms through the Android compatibility package. + * * Simple version of {@link #showDialog(int, Bundle)} that does not * take any arguments. Simply calls {@link #showDialog(int, Bundle)} * with null arguments. */ + @Deprecated public final void showDialog(int id) { showDialog(id, null); } /** + * @deprecated Use the new {@link DialogFragment} class with + * {@link FragmentManager} instead; this is also + * available on older platforms through the Android compatibility package. + * * Show a dialog managed by this activity. A call to {@link #onCreateDialog(int, Bundle)} * will be made with the same id the first time this is called for a given * id. From thereafter, the dialog will be automatically saved and restored. @@ -2815,6 +2834,7 @@ public class Activity extends ContextThemeWrapper * @see #dismissDialog(int) * @see #removeDialog(int) */ + @Deprecated public final boolean showDialog(int id, Bundle args) { if (mManagedDialogs == null) { mManagedDialogs = new SparseArray<ManagedDialog>(); @@ -2836,6 +2856,10 @@ public class Activity extends ContextThemeWrapper } /** + * @deprecated Use the new {@link DialogFragment} class with + * {@link FragmentManager} instead; this is also + * available on older platforms through the Android compatibility package. + * * Dismiss a dialog that was previously shown via {@link #showDialog(int)}. * * @param id The id of the managed dialog. @@ -2848,6 +2872,7 @@ public class Activity extends ContextThemeWrapper * @see #showDialog(int) * @see #removeDialog(int) */ + @Deprecated public final void dismissDialog(int id) { if (mManagedDialogs == null) { throw missingDialog(id); @@ -2870,6 +2895,10 @@ public class Activity extends ContextThemeWrapper } /** + * @deprecated Use the new {@link DialogFragment} class with + * {@link FragmentManager} instead; this is also + * available on older platforms through the Android compatibility package. + * * Removes any internal references to a dialog managed by this Activity. * If the dialog is showing, it will dismiss it as part of the clean up. * @@ -2887,6 +2916,7 @@ public class Activity extends ContextThemeWrapper * @see #showDialog(int) * @see #dismissDialog(int) */ + @Deprecated public final void removeDialog(int id) { if (mManagedDialogs != null) { final ManagedDialog md = mManagedDialogs.get(id); @@ -3597,7 +3627,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)) { @@ -4533,7 +4563,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 b6581e9..fca6868 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; @@ -204,13 +205,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. * @@ -240,7 +234,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. @@ -395,10 +389,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; @@ -614,7 +716,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! @@ -1269,4 +1371,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 6426635..4b09b34c 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); @@ -1398,6 +1397,37 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM 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; + } + } return super.onTransact(code, data, reply, flags); @@ -1410,11 +1440,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; @@ -1831,16 +1861,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(); @@ -3142,5 +3172,46 @@ class ActivityManagerProxy implements IActivityManager return result; } + 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 bd83762..4dfba91 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; @@ -124,7 +124,7 @@ 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; @@ -334,6 +334,7 @@ public final class ActivityThread { private static final class ServiceArgsData { IBinder token; + boolean taskRemoved; int startId; int flags; Intent args; @@ -362,11 +363,10 @@ public final class ActivityThread { } private static final class DumpComponentInfo { - FileDescriptor fd; + ParcelFileDescriptor fd; IBinder token; String prefix; String[] args; - boolean dumped; } private static final class ResultData { @@ -393,6 +393,8 @@ public final class ActivityThread { ParcelFileDescriptor fd; } + native private void dumpGraphicsInfo(FileDescriptor fd); + private final class ApplicationThread extends ApplicationThreadNative { private static final String HEAP_COLUMN = "%17s %8s %8s %8s %8s"; private static final String ONE_COUNT_COLUMN = "%17s %8d"; @@ -533,10 +535,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; @@ -618,20 +621,13 @@ public final class ActivityThread { public void dumpService(FileDescriptor fd, IBinder servicetoken, String[] args) { DumpComponentInfo data = new DumpComponentInfo(); - data.fd = fd; - data.token = servicetoken; - data.args = args; - data.dumped = false; - queueOrSendMessage(H.DUMP_SERVICE, data); - synchronized (data) { - while (!data.dumped) { - try { - data.wait(); - } catch (InterruptedException e) { - // no need to do anything here, we will keep waiting until - // dumped is set - } - } + try { + data.fd = ParcelFileDescriptor.dup(fd); + data.token = servicetoken; + data.args = args; + queueOrSendMessage(H.DUMP_SERVICE, data); + } catch (IOException e) { + Slog.w(TAG, "dumpService failed", e); } } @@ -693,26 +689,24 @@ public final class ActivityThread { public void dumpActivity(FileDescriptor fd, IBinder activitytoken, String prefix, String[] args) { DumpComponentInfo data = new DumpComponentInfo(); - data.fd = fd; - data.token = activitytoken; - data.prefix = prefix; - data.args = args; - data.dumped = false; - queueOrSendMessage(H.DUMP_ACTIVITY, data); - synchronized (data) { - while (!data.dumped) { - try { - data.wait(); - } catch (InterruptedException e) { - // no need to do anything here, we will keep waiting until - // dumped is set - } - } + try { + data.fd = ParcelFileDescriptor.dup(fd); + data.token = activitytoken; + data.prefix = prefix; + data.args = args; + queueOrSendMessage(H.DUMP_ACTIVITY, data); + } catch (IOException e) { + 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; @@ -915,7 +909,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; @@ -1129,8 +1123,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; @@ -2105,33 +2099,27 @@ public final class ActivityThread { } private void handleDumpService(DumpComponentInfo info) { - try { - Service s = mServices.get(info.token); - if (s != null) { - PrintWriter pw = new PrintWriter(new FileOutputStream(info.fd)); - s.dump(info.fd, pw, info.args); - pw.close(); - } - } finally { - synchronized (info) { - info.dumped = true; - info.notifyAll(); + Service s = mServices.get(info.token); + if (s != null) { + PrintWriter pw = new PrintWriter(new FileOutputStream(info.fd.getFileDescriptor())); + s.dump(info.fd.getFileDescriptor(), pw, info.args); + pw.flush(); + try { + info.fd.close(); + } catch (IOException e) { } } } private void handleDumpActivity(DumpComponentInfo info) { - try { - ActivityClientRecord r = mActivities.get(info.token); - if (r != null && r.activity != null) { - PrintWriter pw = new PrintWriter(new FileOutputStream(info.fd)); - r.activity.dump(info.prefix, info.fd, pw, info.args); - pw.close(); - } - } finally { - synchronized (info) { - info.dumped = true; - info.notifyAll(); + ActivityClientRecord r = mActivities.get(info.token); + if (r != null && r.activity != null) { + PrintWriter pw = new PrintWriter(new FileOutputStream(info.fd.getFileDescriptor())); + r.activity.dump(info.prefix, info.fd.getFileDescriptor(), pw, info.args); + pw.flush(); + try { + info.fd.close(); + } catch (IOException e) { } } } @@ -2143,7 +2131,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(); @@ -2673,7 +2667,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); } @@ -3395,8 +3389,7 @@ public final class ActivityThread { } final void handleLowMemory() { - ArrayList<ComponentCallbacks> callbacks - = new ArrayList<ComponentCallbacks>(); + ArrayList<ComponentCallbacks> callbacks; synchronized (mPackages) { callbacks = collectComponentCallbacksLocked(true, null); @@ -3427,6 +3420,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 @@ -3874,7 +3875,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); diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 50e56c7..85918cf 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -33,13 +33,13 @@ 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.PermissionGroupInfo; 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; @@ -1106,6 +1106,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 aa26b04..0e511f2 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -219,6 +219,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; @@ -227,7 +228,7 @@ public abstract class ApplicationThreadNative extends Binder } else { args = null; } - scheduleServiceArgs(token, startId, fl, args); + scheduleServiceArgs(token, taskRemoved, startId, fl, args); return true; } @@ -688,11 +689,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) { @@ -822,7 +824,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeFileDescriptor(fd); data.writeStrongBinder(token); data.writeStringArray(args); - mRemote.transact(DUMP_SERVICE_TRANSACTION, data, null, 0); + mRemote.transact(DUMP_SERVICE_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); } @@ -944,7 +946,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeStrongBinder(token); data.writeString(prefix); data.writeStringArray(args); - mRemote.transact(DUMP_ACTIVITY_TRANSACTION, data, null, 0); + mRemote.transact(DUMP_ACTIVITY_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); } diff --git a/core/java/android/app/BackStackRecord.java b/core/java/android/app/BackStackRecord.java index 1d217f0..e5a7980 100644 --- a/core/java/android/app/BackStackRecord.java +++ b/core/java/android/app/BackStackRecord.java @@ -43,7 +43,7 @@ final class BackStackState implements Parcelable { if (op.removed != null) numRemoved += op.removed.size(); op = op.next; } - mOps = new int[bse.mNumOp*5 + numRemoved]; + mOps = new int[bse.mNumOp*7 + numRemoved]; if (!bse.mAddToBackStack) { throw new IllegalStateException("Not on back stack"); @@ -56,6 +56,8 @@ final class BackStackState implements Parcelable { mOps[pos++] = op.fragment.mIndex; mOps[pos++] = op.enterAnim; mOps[pos++] = op.exitAnim; + mOps[pos++] = op.popEnterAnim; + mOps[pos++] = op.popExitAnim; if (op.removed != null) { final int N = op.removed.size(); mOps[pos++] = N; @@ -101,6 +103,8 @@ final class BackStackState implements Parcelable { op.fragment = f; op.enterAnim = mOps[pos++]; op.exitAnim = mOps[pos++]; + op.popEnterAnim = mOps[pos++]; + op.popExitAnim = mOps[pos++]; final int N = mOps[pos++]; if (N > 0) { op.removed = new ArrayList<Fragment>(N); @@ -169,6 +173,8 @@ final class BackStackRecord extends FragmentTransaction implements static final int OP_REMOVE = 3; static final int OP_HIDE = 4; static final int OP_SHOW = 5; + static final int OP_DETACH = 6; + static final int OP_ATTACH = 7; static final class Op { Op next; @@ -177,6 +183,8 @@ final class BackStackRecord extends FragmentTransaction implements Fragment fragment; int enterAnim; int exitAnim; + int popEnterAnim; + int popExitAnim; ArrayList<Fragment> removed; } @@ -185,6 +193,8 @@ final class BackStackRecord extends FragmentTransaction implements int mNumOp; int mEnterAnim; int mExitAnim; + int mPopEnterAnim; + int mPopExitAnim; int mTransition; int mTransitionStyle; boolean mAddToBackStack; @@ -241,6 +251,11 @@ final class BackStackRecord extends FragmentTransaction implements writer.print(prefix); writer.print("enterAnim="); writer.print(op.enterAnim); writer.print(" exitAnim="); writer.println(op.exitAnim); } + if (op.popEnterAnim != 0 || op.popExitAnim != 0) { + writer.print(prefix); + writer.print("popEnterAnim="); writer.print(op.popEnterAnim); + writer.print(" popExitAnim="); writer.println(op.popExitAnim); + } if (op.removed != null && op.removed.size() > 0) { for (int i=0; i<op.removed.size(); i++) { writer.print(innerPrefix); @@ -299,6 +314,8 @@ final class BackStackRecord extends FragmentTransaction implements } op.enterAnim = mEnterAnim; op.exitAnim = mExitAnim; + op.popEnterAnim = mPopEnterAnim; + op.popExitAnim = mPopExitAnim; mNumOp++; } @@ -401,9 +418,42 @@ final class BackStackRecord extends FragmentTransaction implements return this; } + public FragmentTransaction detach(Fragment fragment) { + //if (fragment.mImmediateActivity == null) { + // throw new IllegalStateException("Fragment not added: " + fragment); + //} + + Op op = new Op(); + op.cmd = OP_DETACH; + op.fragment = fragment; + addOp(op); + + return this; + } + + public FragmentTransaction attach(Fragment fragment) { + //if (fragment.mImmediateActivity == null) { + // throw new IllegalStateException("Fragment not added: " + fragment); + //} + + Op op = new Op(); + op.cmd = OP_ATTACH; + op.fragment = fragment; + addOp(op); + + return this; + } + public FragmentTransaction setCustomAnimations(int enter, int exit) { + return setCustomAnimations(enter, exit, 0, 0); + } + + public FragmentTransaction setCustomAnimations(int enter, int exit, + int popEnter, int popExit) { mEnterAnim = enter; mExitAnim = exit; + mPopEnterAnim = popEnter; + mPopExitAnim = popExit; return this; } @@ -567,6 +617,16 @@ final class BackStackRecord extends FragmentTransaction implements f.mNextAnim = op.enterAnim; mManager.showFragment(f, mTransition, mTransitionStyle); } break; + case OP_DETACH: { + Fragment f = op.fragment; + f.mNextAnim = op.exitAnim; + mManager.detachFragment(f, mTransition, mTransitionStyle); + } break; + case OP_ATTACH: { + Fragment f = op.fragment; + f.mNextAnim = op.enterAnim; + mManager.attachFragment(f, mTransition, mTransitionStyle); + } break; default: { throw new IllegalArgumentException("Unknown cmd: " + op.cmd); } @@ -593,6 +653,7 @@ final class BackStackRecord extends FragmentTransaction implements switch (op.cmd) { case OP_ADD: { Fragment f = op.fragment; + f.mNextAnim = op.popExitAnim; f.mImmediateActivity = null; mManager.removeFragment(f, FragmentManagerImpl.reverseTransit(mTransition), @@ -600,6 +661,7 @@ final class BackStackRecord extends FragmentTransaction implements } break; case OP_REPLACE: { Fragment f = op.fragment; + f.mNextAnim = op.popExitAnim; f.mImmediateActivity = null; mManager.removeFragment(f, FragmentManagerImpl.reverseTransit(mTransition), @@ -607,6 +669,7 @@ final class BackStackRecord extends FragmentTransaction implements if (op.removed != null) { for (int i=0; i<op.removed.size(); i++) { Fragment old = op.removed.get(i); + old.mNextAnim = op.popEnterAnim; f.mImmediateActivity = mManager.mActivity; mManager.addFragment(old, false); } @@ -614,19 +677,32 @@ final class BackStackRecord extends FragmentTransaction implements } break; case OP_REMOVE: { Fragment f = op.fragment; + f.mNextAnim = op.popEnterAnim; f.mImmediateActivity = mManager.mActivity; mManager.addFragment(f, false); } break; case OP_HIDE: { Fragment f = op.fragment; + f.mNextAnim = op.popEnterAnim; mManager.showFragment(f, FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); } break; case OP_SHOW: { Fragment f = op.fragment; + f.mNextAnim = op.popExitAnim; mManager.hideFragment(f, FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); } break; + case OP_DETACH: { + Fragment f = op.fragment; + mManager.attachFragment(f, + FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); + } break; + case OP_ATTACH: { + Fragment f = op.fragment; + mManager.detachFragment(f, + FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle); + } break; default: { throw new IllegalArgumentException("Unknown cmd: " + op.cmd); } diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java index 53dc7c8..6f0bbd7 100644 --- a/core/java/android/app/Fragment.java +++ b/core/java/android/app/Fragment.java @@ -52,6 +52,7 @@ final class FragmentState implements Parcelable { final int mContainerId; final String mTag; final boolean mRetainInstance; + final boolean mDetached; final Bundle mArguments; Bundle mSavedFragmentState; @@ -66,6 +67,7 @@ final class FragmentState implements Parcelable { mContainerId = frag.mContainerId; mTag = frag.mTag; mRetainInstance = frag.mRetainInstance; + mDetached = frag.mDetached; mArguments = frag.mArguments; } @@ -77,6 +79,7 @@ final class FragmentState implements Parcelable { mContainerId = in.readInt(); mTag = in.readString(); mRetainInstance = in.readInt() != 0; + mDetached = in.readInt() != 0; mArguments = in.readBundle(); mSavedFragmentState = in.readBundle(); } @@ -103,6 +106,7 @@ final class FragmentState implements Parcelable { mInstance.mContainerId = mContainerId; mInstance.mTag = mTag; mInstance.mRetainInstance = mRetainInstance; + mInstance.mDetached = mDetached; mInstance.mFragmentManager = activity.mFragments; return mInstance; @@ -120,6 +124,7 @@ final class FragmentState implements Parcelable { dest.writeInt(mContainerId); dest.writeString(mTag); dest.writeInt(mRetainInstance ? 1 : 0); + dest.writeInt(mDetached ? 1 : 0); dest.writeBundle(mArguments); dest.writeBundle(mSavedFragmentState); } @@ -159,11 +164,21 @@ final class FragmentState implements Parcelable { * * <p>Topics covered here: * <ol> + * <li><a href="#OlderPlatforms">Older Platforms</a> * <li><a href="#Lifecycle">Lifecycle</a> * <li><a href="#Layout">Layout</a> * <li><a href="#BackStack">Back Stack</a> * </ol> * + * <a name="OlderPlatforms"></a> + * <h3>Older Platforms</h3> + * + * While the Fragment API was introduced in + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, a version of the API + * is also available for use on older platforms. See the blog post + * <a href="http://android-developers.blogspot.com/2011/03/fragments-for-all.html"> + * Fragments For All</a> for more details. + * * <a name="Lifecycle"></a> * <h3>Lifecycle</h3> * @@ -321,8 +336,9 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener static final int INITIALIZING = 0; // Not yet created. static final int CREATED = 1; // Created. static final int ACTIVITY_CREATED = 2; // The activity has finished its creation. - static final int STARTED = 3; // Created and started, not resumed. - static final int RESUMED = 4; // Created started and resumed. + static final int STOPPED = 3; // Fully created, not started. + static final int STARTED = 4; // Created and started, not resumed. + static final int RESUMED = 5; // Created started and resumed. int mState = INITIALIZING; @@ -404,6 +420,9 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener // from the user. boolean mHidden; + // Set to true when the app has requested that this fragment be detached. + boolean mDetached; + // If set this fragment would like its instance retained across // configuration changes. boolean mRetainInstance; @@ -511,23 +530,27 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener } } - void restoreViewState() { + final void restoreViewState() { if (mSavedViewState != null) { mView.restoreHierarchyState(mSavedViewState); mSavedViewState = null; } } - void setIndex(int index) { + final void setIndex(int index) { mIndex = index; mWho = "android:fragment:" + mIndex; } - void clearIndex() { + final void clearIndex() { mIndex = -1; mWho = null; } + final boolean isInBackStack() { + return mBackStackNesting > 0; + } + /** * Subclasses can not override equals(). */ @@ -947,6 +970,19 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener } /** + * Called immediately after {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} + * has returned, but before any saved state has been restored in to the view. + * This gives subclasses a chance to initialize themselves once + * they know their view hierarchy has been completely created. The fragment's + * view hierarchy is not however attached to its parent at this point. + * @param view The View returned by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + */ + public void onViewCreated(View view, Bundle savedInstanceState) { + } + + /** * Called to have the fragment instantiate its user interface view. * This is optional, and non-graphical fragments can return null (which * is the default implementation). This will be called between @@ -1280,6 +1316,7 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener writer.print(" mFromLayout="); writer.print(mFromLayout); writer.print(" mInLayout="); writer.println(mInLayout); writer.print(prefix); writer.print("mHidden="); writer.print(mHidden); + writer.print(" mDetached="); writer.print(mDetached); writer.print(" mRetainInstance="); writer.print(mRetainInstance); writer.print(" mRetaining="); writer.print(mRetaining); writer.print(" mHasMenu="); writer.println(mHasMenu); diff --git a/core/java/android/app/FragmentManager.java b/core/java/android/app/FragmentManager.java index ab60cf0..0da656f 100644 --- a/core/java/android/app/FragmentManager.java +++ b/core/java/android/app/FragmentManager.java @@ -714,13 +714,14 @@ final class FragmentManagerImpl extends FragmentManager { null, f.mSavedFragmentState); if (f.mView != null) { f.mView.setSaveFromParentEnabled(false); + if (f.mHidden) f.mView.setVisibility(View.GONE); f.restoreViewState(); - if (f.mHidden) f.mView.setVisibility(View.GONE); + f.onViewCreated(f.mView, f.mSavedFragmentState); } } case Fragment.CREATED: if (newState > Fragment.CREATED) { - if (DEBUG) Log.v(TAG, "moveto CONTENT: " + f); + if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f); if (!f.mFromLayout) { ViewGroup container = null; if (f.mContainerId != 0) { @@ -744,9 +745,10 @@ final class FragmentManagerImpl extends FragmentManager { anim.start(); } container.addView(f.mView); - f.restoreViewState(); } - if (f.mHidden) f.mView.setVisibility(View.GONE); + if (f.mHidden) f.mView.setVisibility(View.GONE); + f.restoreViewState(); + f.onViewCreated(f.mView, f.mSavedFragmentState); } } @@ -756,10 +758,13 @@ final class FragmentManagerImpl extends FragmentManager { throw new SuperNotCalledException("Fragment " + f + " did not call through to super.onActivityCreated()"); } + if (f.mView != null) { + } f.mSavedFragmentState = null; } case Fragment.ACTIVITY_CREATED: - if (newState > Fragment.ACTIVITY_CREATED) { + case Fragment.STOPPED: + if (newState > Fragment.STOPPED) { if (DEBUG) Log.v(TAG, "moveto STARTED: " + f); f.mCalled = false; f.onStart(); @@ -803,9 +808,10 @@ final class FragmentManagerImpl extends FragmentManager { + " did not call through to super.onStop()"); } } + case Fragment.STOPPED: case Fragment.ACTIVITY_CREATED: if (newState < Fragment.ACTIVITY_CREATED) { - if (DEBUG) Log.v(TAG, "movefrom CONTENT: " + f); + if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f); if (f.mView != null) { // Need to save the current view state if not // done already. @@ -971,32 +977,36 @@ final class FragmentManagerImpl extends FragmentManager { if (mAdded == null) { mAdded = new ArrayList<Fragment>(); } - mAdded.add(fragment); - makeActive(fragment); if (DEBUG) Log.v(TAG, "add: " + fragment); - fragment.mAdded = true; - fragment.mRemoving = false; - if (fragment.mHasMenu) { - mNeedMenuInvalidate = true; - } - if (moveToStateNow) { - moveToState(fragment); + makeActive(fragment); + if (!fragment.mDetached) { + mAdded.add(fragment); + fragment.mAdded = true; + fragment.mRemoving = false; + if (fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + if (moveToStateNow) { + moveToState(fragment); + } } } public void removeFragment(Fragment fragment, int transition, int transitionStyle) { if (DEBUG) Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting); - mAdded.remove(fragment); - final boolean inactive = fragment.mBackStackNesting <= 0; - if (fragment.mHasMenu) { - mNeedMenuInvalidate = true; - } - fragment.mAdded = false; - fragment.mRemoving = true; - moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED, - transition, transitionStyle); - if (inactive) { - makeInactive(fragment); + final boolean inactive = !fragment.isInBackStack(); + if (!fragment.mDetached || inactive) { + mAdded.remove(fragment); + if (fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + fragment.mAdded = false; + fragment.mRemoving = true; + moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED, + transition, transitionStyle); + if (inactive) { + makeInactive(fragment); + } } } @@ -1052,6 +1062,39 @@ final class FragmentManagerImpl extends FragmentManager { } } + public void detachFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "detach: " + fragment); + if (!fragment.mDetached) { + fragment.mDetached = true; + if (fragment.mAdded) { + // We are not already in back stack, so need to remove the fragment. + mAdded.remove(fragment); + if (fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + fragment.mAdded = false; + fragment.mRemoving = true; + moveToState(fragment, Fragment.CREATED, transition, transitionStyle); + } + } + } + + public void attachFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "attach: " + fragment); + if (fragment.mDetached) { + fragment.mDetached = false; + if (!fragment.mAdded) { + mAdded.add(fragment); + fragment.mAdded = true; + fragment.mRemoving = false; + if (fragment.mHasMenu) { + mNeedMenuInvalidate = true; + } + moveToState(fragment, mCurState, transition, transitionStyle); + } + } + } + public Fragment findFragmentById(int id) { if (mActive != null) { // First look through added fragments. @@ -1594,7 +1637,7 @@ final class FragmentManagerImpl extends FragmentManager { } public void dispatchStop() { - moveToState(Fragment.ACTIVITY_CREATED, false); + moveToState(Fragment.STOPPED, false); } public void dispatchDestroy() { diff --git a/core/java/android/app/FragmentTransaction.java b/core/java/android/app/FragmentTransaction.java index 0cc774d..c1f3cd6 100644 --- a/core/java/android/app/FragmentTransaction.java +++ b/core/java/android/app/FragmentTransaction.java @@ -87,6 +87,31 @@ public abstract class FragmentTransaction { public abstract FragmentTransaction show(Fragment fragment); /** + * Detach the given fragment from the UI. This is the same state as + * when it is put on the back stack: the fragment is removed from + * the UI, however its state is still being actively managed by the + * fragment manager. When going into this state its view hierarchy + * is destroyed. + * + * @param fragment The fragment to be detached. + * + * @return Returns the same FragmentTransaction instance. + */ + public abstract FragmentTransaction detach(Fragment fragment); + + /** + * Re-attach a fragment after it had previously been deatched from + * the UI with {@link #detach(Fragment)}. This + * causes its view hierarchy to be re-created, attached to the UI, + * and displayed. + * + * @param fragment The fragment to be attached. + * + * @return Returns the same FragmentTransaction instance. + */ + public abstract FragmentTransaction attach(Fragment fragment); + + /** * @return <code>true</code> if this transaction contains no operations, * <code>false</code> otherwise. */ @@ -116,10 +141,20 @@ public abstract class FragmentTransaction { /** * Set specific animation resources to run for the fragments that are - * entering and exiting in this transaction. + * entering and exiting in this transaction. These animations will not be + * played when popping the back stack. */ public abstract FragmentTransaction setCustomAnimations(int enter, int exit); - + + /** + * Set specific animation resources to run for the fragments that are + * entering and exiting in this transaction. The <code>popEnter</code> + * and <code>popExit</code> animations will be played for enter/exit + * operations specifically when popping the back stack. + */ + public abstract FragmentTransaction setCustomAnimations(int enter, int exit, + int popEnter, int popExit); + /** * Select a standard transition animation for this transaction. May be * one of {@link #TRANSIT_NONE}, {@link #TRANSIT_FRAGMENT_OPEN}, diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 61e6fc8..bec697a 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; @@ -342,6 +342,13 @@ public interface IActivityManager extends IInterface { public int startActivitiesInPackage(int uid, Intent[] intents, String[] resolvedTypes, IBinder resultTo) 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 */ @@ -515,7 +522,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; @@ -557,4 +564,7 @@ public interface IActivityManager extends IInterface { int START_ACTIVITIES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+120; int START_ACTIVITIES_IN_PACKAGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+121; int ACTIVITY_SLEPT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+122; + int SWITCH_USER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+123; + int REMOVE_SUB_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+124; + int REMOVE_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+125; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 55177a9..b29b088 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -73,8 +73,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/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/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index a601fbf..0fe7b5c 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -35,6 +35,12 @@ public class KeyguardManager { private IWindowManager mWM; /** + * @deprecated Use {@link android.view.WindowManager.LayoutParams#FLAG_DISMISS_KEYGUARD} + * and/or {@link android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} + * instead; this allows you to seamlessly hide the keyguard as your application + * moves in and out of the foreground and does not require that any special + * permissions be requested. + * * Handle returned by {@link KeyguardManager#newKeyguardLock} that allows * you to disable / reenable the keyguard. */ @@ -103,6 +109,12 @@ public class KeyguardManager { } /** + * @deprecated Use {@link android.view.WindowManager.LayoutParams#FLAG_DISMISS_KEYGUARD} + * and/or {@link android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} + * instead; this allows you to seamlessly hide the keyguard as your application + * moves in and out of the foreground and does not require that any special + * permissions be requested. + * * Enables you to lock or unlock the keyboard. Get an instance of this class by * calling {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}. * This class is wrapped by {@link android.app.KeyguardManager KeyguardManager}. @@ -112,6 +124,7 @@ public class KeyguardManager { * @return A {@link KeyguardLock} handle to use to disable and reenable the * keyguard. */ + @Deprecated public KeyguardLock newKeyguardLock(String tag) { return new KeyguardLock(tag); } @@ -168,6 +181,12 @@ public class KeyguardManager { } /** + * @deprecated Use {@link android.view.WindowManager.LayoutParams#FLAG_DISMISS_KEYGUARD} + * and/or {@link android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} + * instead; this allows you to seamlessly hide the keyguard as your application + * moves in and out of the foreground and does not require that any special + * permissions be requested. + * * Exit the keyguard securely. The use case for this api is that, after * disabling the keyguard, your app, which was granted permission to * disable the keyguard and show a limited amount of information deemed @@ -181,6 +200,7 @@ public class KeyguardManager { * it is safe to launch anything that would normally be considered safe * once the user has gotten past the keyguard. */ + @Deprecated public void exitKeyguardSecurely(final OnKeyguardExitResult callback) { try { mWM.exitKeyguardSecurely(new IOnKeyguardExitResult.Stub() { diff --git a/core/java/android/app/ListFragment.java b/core/java/android/app/ListFragment.java index 6e2f4b6..a5ee26c 100644 --- a/core/java/android/app/ListFragment.java +++ b/core/java/android/app/ListFragment.java @@ -195,11 +195,11 @@ public class ListFragment extends Fragment { } /** - * Attach to list view once Fragment is ready to run. + * Attach to list view once the view hierarchy has been created. */ @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); ensureList(); } diff --git a/core/java/android/app/LoaderManager.java b/core/java/android/app/LoaderManager.java index 1ee386d..164141c 100644 --- a/core/java/android/app/LoaderManager.java +++ b/core/java/android/app/LoaderManager.java @@ -34,12 +34,18 @@ import java.lang.reflect.Modifier; * {@link android.content.CursorLoader}, however applications are free to write * their own loaders for loading other types of data. * + * While the LoaderManager API was introduced in + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, a version of the API + * is also available for use on older platforms. See the blog post + * <a href="http://android-developers.blogspot.com/2011/03/fragments-for-all.html"> + * Fragments For All</a> for more details. + * * <p>As an example, here is the full implementation of a {@link Fragment} * that displays a {@link android.widget.ListView} containing the results of * a query against the contacts content provider. It uses a * {@link android.content.CursorLoader} to manage the query on the provider. * - * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentListCursorLoader.java + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/LoaderCursor.java * fragment_cursor} */ public abstract class LoaderManager { 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/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/WallpaperBackupHelper.java b/core/java/android/app/backup/WallpaperBackupHelper.java index 55368d6..0c034cf 100644 --- a/core/java/android/app/backup/WallpaperBackupHelper.java +++ b/core/java/android/app/backup/WallpaperBackupHelper.java @@ -19,6 +19,7 @@ package android.app.backup; import android.app.WallpaperManager; import android.content.Context; import android.graphics.BitmapFactory; +import android.graphics.Point; import android.os.ParcelFileDescriptor; import android.util.Slog; import android.view.Display; @@ -70,8 +71,10 @@ public class WallpaperBackupHelper extends FileBackupHelperBase implements Backu if (mDesiredMinWidth <= 0 || mDesiredMinHeight <= 0) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display d = wm.getDefaultDisplay(); - mDesiredMinWidth = d.getWidth(); - mDesiredMinHeight = d.getHeight(); + Point size = new Point(); + d.getSize(size); + mDesiredMinWidth = size.x; + mDesiredMinHeight = size.y; } if (DEBUG) { 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/AsyncTaskLoader.java b/core/java/android/content/AsyncTaskLoader.java index 383cb6b..0b54396 100644 --- a/core/java/android/content/AsyncTaskLoader.java +++ b/core/java/android/content/AsyncTaskLoader.java @@ -27,7 +27,24 @@ import java.io.PrintWriter; import java.util.concurrent.CountDownLatch; /** - * Abstract Loader that provides an {@link AsyncTask} to do the work. + * Abstract Loader that provides an {@link AsyncTask} to do the work. See + * {@link Loader} and {@link android.app.LoaderManager} for more details. + * + * <p>Here is an example implementation of an AsyncTaskLoader subclass that + * loads the currently installed applications from the package manager. This + * implementation takes care of retrieving the application labels and sorting + * its result set from them, monitoring for changes to the installed + * applications, and rebuilding the list when a change in configuration requires + * this (such as a locale change). + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/LoaderCustom.java + * loader} + * + * <p>An example implementation of a fragment that uses the above loader to show + * the currently installed applications in a list is below. + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/LoaderCustom.java + * fragment} * * @param <D> the data type to be loaded. */ 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/Loader.java b/core/java/android/content/Loader.java index a9d6117..4e70b74 100644 --- a/core/java/android/content/Loader.java +++ b/core/java/android/content/Loader.java @@ -26,7 +26,7 @@ import java.io.PrintWriter; /** * An abstract class that performs asynchronous loading of data. While Loaders are active * they should monitor the source of their data and deliver new results when the contents - * change. + * change. See {@link android.app.LoaderManager} for more detail. * * <p><b>Note on threading:</b> Clients of loaders should as a rule perform * any calls on to a Loader from the main thread of their process (that is, @@ -36,7 +36,10 @@ import java.io.PrintWriter; * be done on the main thread.</p> * * <p>Subclasses generally must implement at least {@link #onStartLoading()}, - * {@link #onStopLoading()}, {@link #onForceLoad()}, and {@link #onReset()}. + * {@link #onStopLoading()}, {@link #onForceLoad()}, and {@link #onReset()}.</p> + * + * <p>Most implementations should not derive directly from this class, but + * instead inherit from {@link AsyncTaskLoader}.</p> * * @param <D> The result returned when the load is complete */ @@ -76,8 +79,12 @@ public class Loader<D> { } /** - * Stores away the application context associated with context. Since Loaders can be used - * across multiple activities it's dangerous to store the context directly. + * Stores away the application context associated with context. + * Since Loaders can be used across multiple activities it's dangerous to + * store the context directly; always use {@link #getContext()} to retrieve + * the Loader's Context, don't use the constructor argument directly. + * The Context returned by {@link #getContext} is safe to use across + * Activity instances. * * @param context used to retrieve the application context. */ diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 46f611f..64c437d 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -334,6 +334,12 @@ public class ActivityInfo extends ComponentInfo public static final int CONFIG_UI_MODE = 0x0200; /** * Bit in {@link #configChanges} that indicates that the activity + * can itself handle the screen size. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_SCREEN_SIZE = 0x0400; + /** + * Bit in {@link #configChanges} that indicates that the activity * can itself handle changes to the font scaling factor. Set from the * {@link android.R.attr#configChanges} attribute. This is * not a core resource configutation, but a higher-level value, so its @@ -341,6 +347,37 @@ public class ActivityInfo extends ComponentInfo */ public static final int CONFIG_FONT_SCALE = 0x40000000; + /** @hide + * Unfortunately the constants for config changes in native code are + * different from ActivityInfo. :( Here are the values we should use for the + * native side given the bit we have assigned in ActivityInfo. + */ + public static int[] CONFIG_NATIVE_BITS = new int[] { + 0x0001, // MNC + 0x0002, // MCC + 0x0004, // LOCALE + 0x0008, // TOUCH SCREEN + 0x0010, // KEYBOARD + 0x0020, // KEYBOARD HIDDEN + 0x0040, // NAVIGATION + 0x0080, // ORIENTATION + 0x0800, // SCREEN LAYOUT + 0x1000, // UI MODE + 0x0200, // SCREEN SIZE + }; + /** @hide + * Convert Java change bits to native. + */ + public static int activityInfoConfigToNative(int input) { + int output = 0; + for (int i=0; i<CONFIG_NATIVE_BITS.length; i++) { + if ((input&(1<<i)) != 0) { + output |= CONFIG_NATIVE_BITS[i]; + } + } + return output; + } + /** * Bit mask of kinds of configuration changes that this activity * can handle itself (without being restarted by the system). diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index fbf8f92..11cd446 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -35,6 +35,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; @@ -329,4 +330,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 7ebfda4..564f4f4 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; @@ -396,7 +396,7 @@ public class PackageParser { int cookie = assmgr.addAssetPath(mArchiveSourcePath); if (cookie != 0) { res = new Resources(assmgr, metrics, null); - assmgr.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + assmgr.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml"); assetError = false; @@ -596,7 +596,7 @@ public class PackageParser { AssetManager assmgr = null; try { assmgr = new AssetManager(); - assmgr.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + assmgr.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); int cookie = assmgr.addAssetPath(packageFilePath); parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml"); @@ -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"; @@ -1931,6 +1931,10 @@ public class PackageParser { a.info.configChanges = sa.getInt( com.android.internal.R.styleable.AndroidManifestActivity_configChanges, 0); + if (owner.applicationInfo.targetSdkVersion + < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + a.info.configChanges |= ActivityInfo.CONFIG_SCREEN_SIZE; + } a.info.softInputMode = sa.getInt( com.android.internal.R.styleable.AndroidManifestActivity_windowSoftInputMode, 0); @@ -2470,6 +2474,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 fc6761d..be67e96 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; @@ -653,7 +652,8 @@ public final class AssetManager { public native final void setConfiguration(int mcc, int mnc, String locale, int orientation, int touchscreen, int density, int keyboard, int keyboardHidden, int navigation, int screenWidth, int screenHeight, - int screenLayout, int uiMode, int majorVersion); + int screenWidthDp, int screenHeightDp, int screenLayout, int uiMode, + int majorVersion); /** * Retrieve the resource identifier for the given resource name. diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 47c2623..93b3429 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -245,6 +245,20 @@ public final class Configuration implements Parcelable, Comparable<Configuration */ public int uiMode; + public static final int SCREEN_WIDTH_DP_UNDEFINED = 0; + + /** + * The current width of the available screen space, in dp units. + */ + public int screenWidthDp; + + public static final int SCREEN_HEIGHT_DP_UNDEFINED = 0; + + /** + * The current height of the available screen space, in dp units. + */ + public int screenHeightDp; + /** * @hide Internal book-keeping. */ @@ -282,46 +296,113 @@ public final class Configuration implements Parcelable, Comparable<Configuration orientation = o.orientation; screenLayout = o.screenLayout; uiMode = o.uiMode; + screenWidthDp = o.screenWidthDp; + screenHeightDp = o.screenHeightDp; seq = o.seq; } public String toString() { StringBuilder sb = new StringBuilder(128); - sb.append("{ scale="); + sb.append("{"); sb.append(fontScale); - sb.append(" imsi="); + sb.append("x imsi="); sb.append(mcc); sb.append("/"); sb.append(mnc); - sb.append(" loc="); - sb.append(locale); - sb.append(" touch="); - sb.append(touchscreen); - sb.append(" keys="); - sb.append(keyboard); - sb.append("/"); - sb.append(keyboardHidden); - sb.append("/"); - sb.append(hardKeyboardHidden); - sb.append(" nav="); - sb.append(navigation); - sb.append("/"); - sb.append(navigationHidden); - sb.append(" orien="); - switch(orientation) { - case ORIENTATION_LANDSCAPE: - sb.append("L"); break; - case ORIENTATION_PORTRAIT: - sb.append("P"); break; - default: - sb.append(orientation); - } - sb.append(" layout=0x"); - sb.append(java.lang.Integer.toHexString(screenLayout)); - sb.append(" uiMode=0x"); - sb.append(java.lang.Integer.toHexString(uiMode)); + if (locale != null) { + sb.append(" "); + sb.append(locale); + } else { + sb.append(" (no locale)"); + } + switch (touchscreen) { + case TOUCHSCREEN_UNDEFINED: sb.append(" ?touch"); break; + case TOUCHSCREEN_NOTOUCH: sb.append(" -touch"); break; + case TOUCHSCREEN_STYLUS: sb.append(" stylus"); break; + case TOUCHSCREEN_FINGER: sb.append(" finger"); break; + default: sb.append(" touch="); sb.append(touchscreen); break; + } + switch (keyboard) { + case KEYBOARD_UNDEFINED: sb.append(" ?keyb"); break; + case KEYBOARD_NOKEYS: sb.append(" -keyb"); break; + case KEYBOARD_QWERTY: sb.append(" qwerty"); break; + case KEYBOARD_12KEY: sb.append(" 12key"); break; + default: sb.append(" keys="); sb.append(keyboard); break; + } + switch (keyboardHidden) { + case KEYBOARDHIDDEN_UNDEFINED: sb.append("/?"); break; + case KEYBOARDHIDDEN_NO: sb.append("/v"); break; + case KEYBOARDHIDDEN_YES: sb.append("/h"); break; + case KEYBOARDHIDDEN_SOFT: sb.append("/s"); break; + default: sb.append("/"); sb.append(keyboardHidden); break; + } + switch (hardKeyboardHidden) { + case HARDKEYBOARDHIDDEN_UNDEFINED: sb.append("/?"); break; + case HARDKEYBOARDHIDDEN_NO: sb.append("/v"); break; + case HARDKEYBOARDHIDDEN_YES: sb.append("/h"); break; + default: sb.append("/"); sb.append(hardKeyboardHidden); break; + } + switch (navigation) { + case NAVIGATION_UNDEFINED: sb.append(" ?nav"); break; + case NAVIGATION_NONAV: sb.append(" -nav"); break; + case NAVIGATION_DPAD: sb.append(" dpad"); break; + case NAVIGATION_TRACKBALL: sb.append(" tball"); break; + case NAVIGATION_WHEEL: sb.append(" wheel"); break; + default: sb.append(" nav="); sb.append(navigation); break; + } + switch (navigationHidden) { + case NAVIGATIONHIDDEN_UNDEFINED: sb.append("/?"); break; + case NAVIGATIONHIDDEN_NO: sb.append("/v"); break; + case NAVIGATIONHIDDEN_YES: sb.append("/h"); break; + default: sb.append("/"); sb.append(navigationHidden); break; + } + switch (orientation) { + case ORIENTATION_UNDEFINED: sb.append(" ?orien"); break; + case ORIENTATION_LANDSCAPE: sb.append(" land"); break; + case ORIENTATION_PORTRAIT: sb.append(" port"); break; + default: sb.append(" orien="); sb.append(orientation); break; + } + switch ((screenLayout&SCREENLAYOUT_SIZE_MASK)) { + case SCREENLAYOUT_SIZE_UNDEFINED: sb.append(" ?lsize"); break; + case SCREENLAYOUT_SIZE_SMALL: sb.append(" smll"); break; + case SCREENLAYOUT_SIZE_NORMAL: sb.append(" nrml"); break; + case SCREENLAYOUT_SIZE_LARGE: sb.append(" lrg"); break; + case SCREENLAYOUT_SIZE_XLARGE: sb.append(" xlrg"); break; + default: sb.append(" layoutSize="); + sb.append(screenLayout&SCREENLAYOUT_SIZE_MASK); break; + } + switch ((screenLayout&SCREENLAYOUT_LONG_MASK)) { + case SCREENLAYOUT_LONG_UNDEFINED: sb.append(" ?long"); break; + case SCREENLAYOUT_LONG_NO: /* not-long is not interesting to print */ break; + case SCREENLAYOUT_LONG_YES: sb.append(" long"); break; + default: sb.append(" layoutLong="); + sb.append(screenLayout&SCREENLAYOUT_LONG_MASK); break; + } + switch ((uiMode&UI_MODE_TYPE_MASK)) { + case UI_MODE_TYPE_UNDEFINED: sb.append(" ?uimode"); break; + case UI_MODE_TYPE_NORMAL: /* normal is not interesting to print */ break; + case UI_MODE_TYPE_DESK: sb.append(" desk"); break; + case UI_MODE_TYPE_CAR: sb.append(" car"); break; + default: sb.append(" uimode="); sb.append(uiMode&UI_MODE_TYPE_MASK); break; + } + switch ((uiMode&UI_MODE_NIGHT_MASK)) { + case UI_MODE_NIGHT_UNDEFINED: sb.append(" ?night"); break; + case UI_MODE_NIGHT_NO: /* not-night is not interesting to print */ break; + case UI_MODE_NIGHT_YES: sb.append(" night"); break; + default: sb.append(" night="); sb.append(uiMode&UI_MODE_NIGHT_MASK); break; + } + if (screenWidthDp != SCREEN_WIDTH_DP_UNDEFINED) { + sb.append(" w"); sb.append(screenWidthDp); sb.append("dp"); + } else { + sb.append("?wdp"); + } + if (screenHeightDp != SCREEN_HEIGHT_DP_UNDEFINED) { + sb.append(" h"); sb.append(screenHeightDp); sb.append("dp"); + } else { + sb.append("?hdp"); + } if (seq != 0) { - sb.append(" seq="); + sb.append(" s."); sb.append(seq); } sb.append('}'); @@ -345,6 +426,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration orientation = ORIENTATION_UNDEFINED; screenLayout = SCREENLAYOUT_SIZE_UNDEFINED; uiMode = UI_MODE_TYPE_UNDEFINED; + screenWidthDp = SCREEN_WIDTH_DP_UNDEFINED; + screenHeightDp = SCREEN_HEIGHT_DP_UNDEFINED; seq = 0; } @@ -438,6 +521,16 @@ public final class Configuration implements Parcelable, Comparable<Configuration | (delta.uiMode&UI_MODE_NIGHT_MASK); } } + if (delta.screenWidthDp != SCREEN_WIDTH_DP_UNDEFINED + && screenWidthDp != delta.screenWidthDp) { + changed |= ActivityInfo.CONFIG_SCREEN_SIZE; + screenWidthDp = delta.screenWidthDp; + } + if (delta.screenHeightDp != SCREEN_HEIGHT_DP_UNDEFINED + && screenHeightDp != delta.screenHeightDp) { + changed |= ActivityInfo.CONFIG_SCREEN_SIZE; + screenHeightDp = delta.screenHeightDp; + } if (delta.seq != 0) { seq = delta.seq; @@ -467,9 +560,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration * {@link android.content.pm.ActivityInfo#CONFIG_NAVIGATION * PackageManager.ActivityInfo.CONFIG_NAVIGATION}, * {@link android.content.pm.ActivityInfo#CONFIG_ORIENTATION - * PackageManager.ActivityInfo.CONFIG_ORIENTATION}, or + * PackageManager.ActivityInfo.CONFIG_ORIENTATION}, * {@link android.content.pm.ActivityInfo#CONFIG_SCREEN_LAYOUT - * PackageManager.ActivityInfo.CONFIG_SCREEN_LAYOUT}. + * PackageManager.ActivityInfo.CONFIG_SCREEN_LAYOUT}, or + * {@link android.content.pm.ActivityInfo#CONFIG_SCREEN_SIZE + * PackageManager.ActivityInfo.CONFIG_SCREEN_SIZE}. */ public int diff(Configuration delta) { int changed = 0; @@ -522,6 +617,14 @@ public final class Configuration implements Parcelable, Comparable<Configuration && uiMode != delta.uiMode) { changed |= ActivityInfo.CONFIG_UI_MODE; } + if (delta.screenWidthDp != SCREEN_WIDTH_DP_UNDEFINED + && screenWidthDp != delta.screenWidthDp) { + changed |= ActivityInfo.CONFIG_SCREEN_SIZE; + } + if (delta.screenHeightDp != SCREEN_HEIGHT_DP_UNDEFINED + && screenHeightDp != delta.screenHeightDp) { + changed |= ActivityInfo.CONFIG_SCREEN_SIZE; + } return changed; } @@ -603,6 +706,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration dest.writeInt(orientation); dest.writeInt(screenLayout); dest.writeInt(uiMode); + dest.writeInt(screenWidthDp); + dest.writeInt(screenHeightDp); dest.writeInt(seq); } @@ -624,6 +729,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration orientation = source.readInt(); screenLayout = source.readInt(); uiMode = source.readInt(); + screenWidthDp = source.readInt(); + screenHeightDp = source.readInt(); seq = source.readInt(); } @@ -684,6 +791,10 @@ public final class Configuration implements Parcelable, Comparable<Configuration n = this.screenLayout - that.screenLayout; if (n != 0) return n; n = this.uiMode - that.uiMode; + if (n != 0) return n; + n = this.screenWidthDp - that.screenWidthDp; + if (n != 0) return n; + n = this.screenHeightDp - that.screenHeightDp; //if (n != 0) return n; return n; } @@ -708,6 +819,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration + this.touchscreen + this.keyboard + this.keyboardHidden + this.hardKeyboardHidden + this.navigation + this.navigationHidden - + this.orientation + this.screenLayout + this.uiMode; + + this.orientation + this.screenLayout + this.uiMode + + this.screenWidthDp + this.screenHeightDp; } } diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 81eb09c..2e6ae70 100755 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -21,6 +21,7 @@ import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.content.pm.ActivityInfo; import android.graphics.Movie; import android.graphics.drawable.Drawable; import android.graphics.drawable.ColorDrawable; @@ -1404,6 +1405,7 @@ public class Resources { int configChanges = 0xfffffff; if (config != null) { configChanges = mConfiguration.updateFrom(config); + configChanges = ActivityInfo.activityInfoConfigToNative(configChanges); } if (mConfiguration.locale == null) { mConfiguration.locale = Locale.getDefault(); @@ -1443,6 +1445,7 @@ public class Resources { mConfiguration.touchscreen, (int)(mMetrics.density*160), mConfiguration.keyboard, keyboardHidden, mConfiguration.navigation, width, height, + mConfiguration.screenWidthDp, mConfiguration.screenHeightDp, mConfiguration.screenLayout, mConfiguration.uiMode, Build.VERSION.RESOURCES_SDK_INT); 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/hardware/Camera.java b/core/java/android/hardware/Camera.java index 97f0e1b..77c2d1b 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,52 @@ public class Camera { }; /** + * Area class for focus. + * + * @see #setFocusAreas(List) + * @see #getFocusAreas() + * @hide + */ + 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 +1171,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 +1180,8 @@ 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_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"; @@ -1462,6 +1520,27 @@ public class Camera { mMap.put(key, Integer.toString(value)); } + private void set(String key, List<Area> areas) { + 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. * @@ -2492,6 +2571,144 @@ 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() + * @hide + */ + 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 all-zero single 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 + * @hide + */ + public List<Area> getFocusAreas() { + return splitArea(KEY_FOCUS_AREAS); + } + + /** + * Sets focus areas. See {@link #getFocusAreas()} for documentation. + * + * @param focusAreas the focus areas + * @see #getFocusAreas() + * @hide + */ + 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() + * @hide + */ + 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 all-zero single 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 + * @hide + */ + public List<Area> getMeteringAreas() { + return splitArea(KEY_METERING_AREAS); + } + + /** + * Sets metering areas. See {@link #getMeteringAreas()} for + * documentation. + * + * @param meteringAreas the metering areas + * @see #getMeteringAreas() + * @hide + */ + 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 +2834,31 @@ 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. + 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; + return result; + } }; } diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java index f2b907b..a4ba3bd 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; /** @@ -97,6 +104,9 @@ public class Sensor { */ public static final int TYPE_ROTATION_VECTOR = 11; + /** 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 78d7991..91f0098 100644 --- a/core/java/android/hardware/SensorEvent.java +++ b/core/java/android/hardware/SensorEvent.java @@ -305,6 +305,14 @@ public class SensorEvent { * positive in the counter-clockwise direction). * </p> * + * <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/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/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/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 90e2e79..b0f3ac3 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(""); } @@ -1918,6 +2115,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 7dc36f9..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; @@ -256,6 +255,25 @@ public class Binder implements IBinder { } /** + * Like {@link #dump(FileDescriptor, String[])}, but ensures the target + * executes asynchronously. + */ + public void dumpAsync(final FileDescriptor fd, final String[] args) { + final FileOutputStream fout = new FileOutputStream(fd); + final PrintWriter pw = new PrintWriter(fout); + Thread thr = new Thread("Binder.dumpAsync") { + public void run() { + try { + dump(fd, pw, args); + } finally { + pw.flush(); + } + } + }; + thr.start(); + } + + /** * Print the object's state into the given stream. * * @param fd The raw file descriptor that the dump is being sent to. @@ -272,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); } @@ -364,6 +382,20 @@ final class BinderProxy implements IBinder { } } + public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeFileDescriptor(fd); + data.writeStringArray(args); + try { + transact(DUMP_TRANSACTION, data, reply, FLAG_ONEWAY); + reply.readException(); + } finally { + data.recycle(); + reply.recycle(); + } + } + BinderProxy() { mSelf = new WeakReference(this); } @@ -380,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 3bb0821..24d5369 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -230,6 +230,11 @@ public class Build { * Newest version of Android, version 3.1. */ public static final int HONEYCOMB_MR1 = 12; + + /** + * 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/IBinder.java b/core/java/android/os/IBinder.java index 8ae8008..8876354 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -157,6 +157,16 @@ public interface IBinder { public void dump(FileDescriptor fd, String[] args) throws RemoteException; /** + * Like {@link #dump(FileDescriptor, String[])} but always executes + * asynchronously. If the object is local, a new thread is created + * to perform the dump. + * + * @param fd The raw file descriptor that the dump is being sent to. + * @param args additional arguments to the dump request. + */ + public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException; + + /** * Perform a generic operation with the object. * * @param code The action to perform. This should 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/Parcel.java b/core/java/android/os/Parcel.java index eca3484..6b35215 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -1383,6 +1383,8 @@ public final class Parcel { private native FileDescriptor internalReadFileDescriptor(); /*package*/ static native FileDescriptor openFileDescriptor(String file, int mode) throws FileNotFoundException; + /*package*/ static native FileDescriptor dupFileDescriptor(FileDescriptor orig) + throws IOException; /*package*/ static native void closeFileDescriptor(FileDescriptor desc) throws IOException; /*package*/ static native void clearFileDescriptor(FileDescriptor desc); diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 5bd129f..f7661b6 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -35,64 +35,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,17 +106,28 @@ 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; } /** + * Create a new ParcelFileDescriptor that is a dup of an existing + * FileDescriptor. This obeys standard POSIX semantics, where the + * new file descriptor shared state such as file position with the + * original file descriptor. + */ + public static ParcelFileDescriptor dup(FileDescriptor orig) throws IOException { + FileDescriptor fd = Parcel.dupFileDescriptor(orig); + return fd != null ? new ParcelFileDescriptor(fd) : null; + } + + /** * Create a new ParcelFileDescriptor from the specified Socket. * * @param socket The Socket whose FileDescriptor is used to create @@ -126,13 +137,10 @@ 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 two ParcelFileDescriptors structured as a data pipe. The first * ParcelFileDescriptor in the returned array is the read side; the second @@ -176,26 +184,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 @@ -207,9 +215,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 @@ -229,11 +237,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. */ @@ -250,7 +258,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 @@ -258,7 +266,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; @@ -273,7 +281,7 @@ public class ParcelFileDescriptor implements Parcelable { } } } - + /** * An OutputStream you can create on a ParcelFileDescriptor, which will * take care of calling {@link ParcelFileDescriptor#close @@ -281,7 +289,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; @@ -296,12 +304,12 @@ public class ParcelFileDescriptor implements Parcelable { } } } - + @Override public String toString() { return "{ParcelFileDescriptor: " + mFileDescriptor + "}"; } - + @Override protected void finalize() throws Throwable { try { @@ -312,13 +320,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) { @@ -327,7 +335,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 efb8415..c830f7a 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 { @@ -130,14 +131,25 @@ public class PowerManager /** * Wake lock that ensures that the screen and keyboard are on at * full brightness. + * + * <p class="note">Most applications should strongly consider using + * {@link android.view.WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON}. + * This window flag will be correctly managed by the platform + * as the user moves between applications and doesn't require a special permission.</p> */ public static final int FULL_WAKE_LOCK = WAKE_BIT_CPU_WEAK | WAKE_BIT_SCREEN_BRIGHT | WAKE_BIT_KEYBOARD_BRIGHT; /** + * @deprecated Most applications should use + * {@link android.view.WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON} instead + * of this type of wake lock, as it will be correctly managed by the platform + * as the user moves between applications and doesn't require a special permission. + * * Wake lock that ensures that the screen is on at full brightness; * the keyboard backlight will be allowed to go off. */ + @Deprecated public static final int SCREEN_BRIGHT_WAKE_LOCK = WAKE_BIT_CPU_WEAK | WAKE_BIT_SCREEN_BRIGHT; /** @@ -188,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 { @@ -380,6 +395,11 @@ public class PowerManager *wl.release(); * } * + * <p class="note">If using this to keep the screen on, you should strongly consider using + * {@link android.view.WindowManager.LayoutParams#FLAG_KEEP_SCREEN_ON} instead. + * This window flag will be correctly managed by the platform + * as the user moves between applications and doesn't require a special permission.</p> + * * @param flags Combination of flag values defining the requested behavior of the WakeLock. * @param tag Your class name (or other tag) for debugging purposes. * 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/os/storage/IMountService.java b/core/java/android/os/storage/IMountService.java index 4c83515..27da3c3 100644 --- a/core/java/android/os/storage/IMountService.java +++ b/core/java/android/os/storage/IMountService.java @@ -637,6 +637,22 @@ public interface IMountService extends IInterface { } return _result; } + + public String[] getVolumeList() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + String[] _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_getVolumeList, _data, _reply, 0); + _reply.readException(); + _result = _reply.readStringArray(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } } private static final String DESCRIPTOR = "IMountService"; @@ -699,6 +715,8 @@ public interface IMountService extends IInterface { static final int TRANSACTION_changeEncryptionPassword = IBinder.FIRST_CALL_TRANSACTION + 28; + static final int TRANSACTION_getVolumeList = IBinder.FIRST_CALL_TRANSACTION + 29; + /** * Cast an IBinder object into an IMountService interface, generating a * proxy if needed. @@ -1004,6 +1022,13 @@ public interface IMountService extends IInterface { reply.writeInt(result); return true; } + case TRANSACTION_getVolumeList: { + data.enforceInterface(DESCRIPTOR); + String[] result = getVolumeList(); + reply.writeNoException(); + reply.writeStringArray(result); + return true; + } } return super.onTransact(code, data, reply, flags); } @@ -1179,4 +1204,8 @@ public interface IMountService extends IInterface { */ public int changeEncryptionPassword(String password) throws RemoteException; + /** + * Returns list of all mountable volumes. + */ + public String[] getVolumeList() throws RemoteException; } diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 73ac79f..234057b 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -527,4 +527,30 @@ public class StorageManager return null; } + + /** + * Gets the state of a volume via its mountpoint. + * @hide + */ + public String getVolumeState(String mountPoint) { + try { + return mMountService.getVolumeState(mountPoint); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get volume state", e); + return null; + } + } + + /** + * Returns list of all mountable volumes. + * @hide + */ + public String[] getVolumeList() { + try { + return mMountService.getVolumeList(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get volume list", e); + return null; + } + } } 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/Calendar.java b/core/java/android/provider/Calendar.java index de71763..309a5ec 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"; } /** @@ -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> */ @@ -212,7 +205,7 @@ 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 @@ -288,29 +281,32 @@ public final class Calendar { 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.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, 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.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); @@ -432,7 +428,19 @@ 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"; } /** @@ -505,6 +513,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> */ @@ -1011,7 +1028,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 b59421e..02fd6e4 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"; @@ -344,6 +346,13 @@ public final class MediaStore { */ public interface FileColumns extends MediaColumns { /** + * The MTP storage ID of the file + * <P>Type: INTEGER</P> + * @hide + */ + public static final String STORAGE_ID = "storage_id"; + + /** * The MTP format code of the file * <P>Type: INTEGER</P> * @hide 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/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..7efc264 --- /dev/null +++ b/core/java/android/speech/tts/FileSynthesisRequest.java @@ -0,0 +1,251 @@ +/* + * 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.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, File fileName) { + super(text); + 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..dc5ff70 --- /dev/null +++ b/core/java/android/speech/tts/PlaybackSynthesisRequest.java @@ -0,0 +1,322 @@ +/* + * 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.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, int streamType, float volume, float pan) { + super(text); + 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..515218b --- /dev/null +++ b/core/java/android/speech/tts/SynthesisRequest.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.speech.tts; + +/** + * 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}. + * + * @hide Pending approval + */ +public abstract class SynthesisRequest { + + private final String mText; + private String mLanguage; + private String mCountry; + private String mVariant; + private int mSpeechRate; + private int mPitch; + + public SynthesisRequest(String text) { + mText = text; + } + + /** + * 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. {@link TextToSpeech.Engine#DEFAULT_RATE} (100) + * is the normal rate. + */ + public int getSpeechRate() { + return mSpeechRate; + } + + /** + * Gets the pitch to use. {@link TextToSpeech.Engine#DEFAULT_PITCH} (100) + * is the normal pitch. + */ + public int getPitch() { + return mPitch; + } + + /** + * 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..e247df8 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,20 +74,17 @@ 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 * 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, * but not the country and variant. @@ -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,117 @@ 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. + * + * @hide Pending API council approval + */ + @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 +249,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 +277,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 +314,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 +322,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,134 +345,87 @@ 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(); - /** - * Used to store the cached parameters sent along with each synthesis request to the - * TTS service. - */ - private String[] mCachedParams; + + private String mRequestedEngine; + private final Map<String, Uri> mEarcons; + private final Map<String, Uri> mUtterances; + private final Bundle mParams = new Bundle(); /** * The constructor for the TextToSpeech class. @@ -449,84 +438,98 @@ public class TextToSpeech { * TextToSpeech engine has initialized. */ public TextToSpeech(Context context, OnInitListener listener) { + this(context, listener, null); + } + + /** + * @hide pending approval + */ + 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; + } + } + + // 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; + } - 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); + 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("TextToSpeech.java", "initTts() failed to bind to service"); - if (mInitListener != null) { - mInitListener.onInit(ERROR); - } + 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"); + 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 +537,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 +576,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 +599,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 +632,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 +653,199 @@ 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}. * - * @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}. * - * @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}. * - * @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; + 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)); } - 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; - } - } + }, 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; + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + return service.stop(getPackageName()); } - 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; - } - } + }, 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 +853,317 @@ 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; - } - 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; + 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()); } - } + }, 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 + * @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; - } - 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); - } - 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; + 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)); } + }, 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); + 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}. + * + * @hide Pending approval */ - 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 + * @hide Pending approval */ - 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..da97fb4 --- /dev/null +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -0,0 +1,714 @@ +/* + * 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. + * + * @hide Pending approval + */ +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; + private 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, 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(), 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/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..6b2d8e4 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -1170,6 +1170,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. 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..1b7f2f3 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; @@ -835,6 +836,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 +870,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 d5010c6..ac5db62 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(); if (kind == 1) @@ -708,6 +713,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); } @@ -760,7 +769,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++) { @@ -1114,7 +1123,6 @@ public class TextUtils { int remaining = commaCount + 1; int ok = 0; - int okRemaining = remaining; String okFormat = ""; int w = 0; @@ -1146,7 +1154,6 @@ public class TextUtils { if (w + moreWid <= avail) { ok = i + 1; - okRemaining = remaining; okFormat = format; } } @@ -1179,6 +1186,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); } } @@ -1537,6 +1545,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..7083641 --- /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 Arrays.copyOf(mSuggestions, mSuggestions.length); + } + + /** + * @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/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/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/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/Display.java b/core/java/android/view/Display.java index 126f409..8e839c0 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -16,10 +16,15 @@ package android.view; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; import android.util.DisplayMetrics; +import android.util.Slog; -public class Display -{ +public class Display { /** * Specify the default Display */ @@ -35,10 +40,10 @@ public class Display Display(int display) { // initalize the statics when this class is first instansiated. This is // done here instead of in the static block because Zygote - synchronized (mStaticInit) { - if (!mInitialized) { + synchronized (sStaticInit) { + if (!sInitialized) { nativeClassInit(); - mInitialized = true; + sInitialized = true; } } mDisplay = display; @@ -60,29 +65,92 @@ public class Display native static int getDisplayCount(); /** - * Returns the raw width of the display, in pixels. Note that this + * Returns the raw size of the display, in pixels. Note that this * should <em>not</em> generally be used for computing layouts, since * a device will typically have screen decoration (such as a status bar) * along the edges of the display that reduce the amount of application * space available from the raw size returned here. This value is * adjusted for you based on the current rotation of the display. */ - native public int getWidth(); + public void getSize(Point outSize) { + try { + IWindowManager wm = getWindowManager(); + if (wm != null) { + wm.getDisplaySize(outSize); + } else { + // This is just for boot-strapping, initializing the + // system process before the window manager is up. + outSize.y = getRealHeight(); + } + } catch (RemoteException e) { + Slog.w("Display", "Unable to get display size", e); + } + } /** - * Returns the raw height of the display, in pixels. Note that this - * should <em>not</em> generally be used for computing layouts, since - * a device will typically have screen decoration (such as a status bar) - * along the edges of the display that reduce the amount of application - * space available from the raw size returned here. This value is - * adjusted for you based on the current rotation of the display. + * This is just easier for some parts of the framework. */ - native public int getHeight(); + public void getRectSize(Rect outSize) { + synchronized (mTmpPoint) { + getSize(mTmpPoint); + outSize.set(0, 0, mTmpPoint.x, mTmpPoint.y); + } + } - /** @hide special for when we are faking the screen size. */ + /** + * Return the maximum screen size dimension that will happen. This is + * mostly for wallpapers. + * @hide + */ + public int getMaximumSizeDimension() { + try { + IWindowManager wm = getWindowManager(); + return wm.getMaximumSizeDimension(); + } catch (RemoteException e) { + Slog.w("Display", "Unable to get display maximum size dimension", e); + return 0; + } + } + + /** + * @deprecated Use {@link #getSize(Point)} instead. + */ + @Deprecated + public int getWidth() { + synchronized (mTmpPoint) { + long now = SystemClock.uptimeMillis(); + if (now > (mLastGetTime+20)) { + getSize(mTmpPoint); + mLastGetTime = now; + } + return mTmpPoint.x; + } + } + + /** + * @deprecated Use {@link #getSize(Point)} instead. + */ + @Deprecated + public int getHeight() { + synchronized (mTmpPoint) { + long now = SystemClock.uptimeMillis(); + if (now > (mLastGetTime+20)) { + getSize(mTmpPoint); + mLastGetTime = now; + } + return mTmpPoint.y; + } + } + + /** @hide Returns the actual screen size, not including any decor. */ native public int getRealWidth(); - /** @hide special for when we are faking the screen size. */ + /** @hide Returns the actual screen size, not including any decor. */ native public int getRealHeight(); + + /** @hide special for when we are faking the screen size. */ + native public int getRawWidth(); + /** @hide special for when we are faking the screen size. */ + native public int getRawHeight(); /** * Returns the rotation of the screen from its "natural" orientation. @@ -132,8 +200,27 @@ public class Display * @param outMetrics */ public void getMetrics(DisplayMetrics outMetrics) { - outMetrics.widthPixels = getWidth(); - outMetrics.heightPixels = getHeight(); + synchronized (mTmpPoint) { + getSize(mTmpPoint); + outMetrics.widthPixels = mTmpPoint.x; + outMetrics.heightPixels = mTmpPoint.y; + } + getNonSizeMetrics(outMetrics); + } + + /** + * Initialize a DisplayMetrics object from this display's data. + * + * @param outMetrics + * @hide + */ + public void getRealMetrics(DisplayMetrics outMetrics) { + outMetrics.widthPixels = getRealWidth(); + outMetrics.heightPixels = getRealHeight(); + getNonSizeMetrics(outMetrics); + } + + private void getNonSizeMetrics(DisplayMetrics outMetrics) { outMetrics.density = mDensity; outMetrics.densityDpi = (int)((mDensity*DisplayMetrics.DENSITY_DEFAULT)+.5f); outMetrics.scaledDensity= outMetrics.density; @@ -141,6 +228,16 @@ public class Display outMetrics.ydpi = mDpiY; } + static IWindowManager getWindowManager() { + synchronized (sStaticInit) { + if (sWindowManager == null) { + sWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService("window")); + } + return sWindowManager; + } + } + /* * We use a class initializer to allow the native code to cache some * field offsets. @@ -157,8 +254,12 @@ public class Display private float mDpiX; private float mDpiY; - private static final Object mStaticInit = new Object(); - private static boolean mInitialized = false; + private final Point mTmpPoint = new Point(); + private float mLastGetTime; + + private static final Object sStaticInit = new Object(); + private static boolean sInitialized = false; + private static IWindowManager sWindowManager; /** * Returns a display object which uses the metric's width/height instead. diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index 14f2e9d..b8c5c2a 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; @@ -259,10 +265,10 @@ class GLES20Canvas extends HardwareCanvas { 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); @@ -455,10 +461,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 +533,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 +551,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 +564,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 +576,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 +589,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 +606,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 +627,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 +661,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 +674,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 +708,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 +723,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 +740,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 +748,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 +773,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 +803,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 +828,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 +843,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 +856,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 +873,7 @@ class GLES20Canvas extends HardwareCanvas { TemporaryBuffer.recycle(buf); } } finally { - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } @@ -872,11 +883,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 +896,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 +926,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 +945,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 +965,7 @@ class GLES20Canvas extends HardwareCanvas { TemporaryBuffer.recycle(buf); } } finally { - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } @@ -968,43 +979,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/GestureDetector.java b/core/java/android/view/GestureDetector.java index c1e1049..1ccc66f 100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -245,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(); @@ -443,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(); @@ -579,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/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index 8584bf2..66f37f2 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -20,7 +20,7 @@ package android.view; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; -import android.os.SystemClock; +import android.os.*; import android.util.EventLog; import android.util.Log; @@ -256,6 +256,7 @@ public abstract class HardwareRenderer { @SuppressWarnings({"deprecation"}) static abstract class GlRenderer extends HardwareRenderer { + // These values are not exposed in our EGL APIs 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; @@ -290,7 +291,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); } @@ -644,8 +645,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(); } } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index dd8242a..0be02a6 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -21,6 +21,7 @@ import com.android.internal.view.IInputMethodClient; import android.content.res.Configuration; import android.graphics.Bitmap; +import android.graphics.Point; import android.view.IApplicationToken; import android.view.IOnKeyguardExitResult; import android.view.IRotationWatcher; @@ -52,6 +53,9 @@ interface IWindowManager in IInputContext inputContext); boolean inputMethodClientHasFocus(IInputMethodClient client); + void getDisplaySize(out Point size); + int getMaximumSizeDimension(); + // These can only be called when injecting events to your own window, // or by holding the INJECT_EVENTS permission. These methods may block // until pending input events are finished being dispatched even when 'sync' is false. diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 98d4eb9..8cb68f9 100755 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -290,7 +290,7 @@ public final class InputDevice implements Parcelable { * @return The input device or null if not found. */ public static InputDevice getDevice(int id) { - IWindowManager wm = IWindowManager.Stub.asInterface(ServiceManager.getService("window")); + IWindowManager wm = Display.getWindowManager(); try { return wm.getInputDevice(id); } catch (RemoteException ex) { @@ -304,7 +304,7 @@ public final class InputDevice implements Parcelable { * @return The input device ids. */ public static int[] getDeviceIds() { - IWindowManager wm = IWindowManager.Stub.asInterface(ServiceManager.getService("window")); + IWindowManager wm = Display.getWindowManager(); try { return wm.getInputDeviceIds(); } catch (RemoteException ex) { 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/KeyCharacterMap.java b/core/java/android/view/KeyCharacterMap.java index 3ff7fcd..885a75f 100644 --- a/core/java/android/view/KeyCharacterMap.java +++ b/core/java/android/view/KeyCharacterMap.java @@ -527,7 +527,7 @@ public class KeyCharacterMap { */ public static boolean[] deviceHasKeys(int[] keyCodes) { boolean[] ret = new boolean[keyCodes.length]; - IWindowManager wm = IWindowManager.Stub.asInterface(ServiceManager.getService("window")); + IWindowManager wm = Display.getWindowManager(); try { wm.hasKeys(keyCodes, ret); } catch (RemoteException e) { diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 81d5a6e..13d8809 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}. * diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index a17db5d..7611b08 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -172,6 +172,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { * 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)}. @@ -184,8 +186,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { * 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></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> * This action is not a touch event so it is delivered to * {@link View#onGenericMotionEvent(MotionEvent)} rather than @@ -195,6 +198,32 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int ACTION_SCROLL = 8; /** + * Constant for {@link #getAction}: 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 #getAction}: 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 @@ -279,6 +308,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; @@ -1025,6 +1065,7 @@ 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); @@ -1261,6 +1302,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 +1401,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 +1421,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. @@ -2313,6 +2374,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) { 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..0a7a375 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>(); 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 5a96efd..5af2e56 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -1304,6 +1304,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 +1316,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 +1629,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 +1655,27 @@ 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; + + /* 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 +1847,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 +2277,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 +2384,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 @@ -3419,6 +3455,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (!isShown()) { return; } + + // Populate these here since they are related to the View that + // sends the event and should not be modified while dispatching + // to descendants. event.setClassName(getClass().getName()); event.setPackageName(getContext().getPackageName()); event.setEnabled(isEnabled()); @@ -3434,22 +3474,38 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility dispatchPopulateAccessibilityEvent(event); - AccessibilityManager.getInstance(mContext).sendAccessibilityEvent(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} children - * to be populated. + * Dispatches an {@link AccessibilityEvent} to the {@link View} children to be populated. + * This method first calls {@link #onPopulateAccessibilityEvent(AccessibilityEvent)} + * on this view allowing it to populate information about itself and also decide + * whether to intercept the population i.e. to prevent its children from populating + * the event. * * @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 evnet with + * information about itself. + * + * @param event The accessibility event which to populate. + */ + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + + } + + /** * Gets the {@link View} description. It briefly describes the view and is * primarily used for accessibility support. Set this property to enable * better accessibility support for your application. This is especially @@ -4562,21 +4618,31 @@ 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. + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onKeyEvent(event, 0); + } //noinspection SimplifiableIfStatement,deprecation - if (android.util.Config.LOGV) { + if (false) { captureViewInfo("captureViewKeyEvent", this); } + // 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 +4663,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; + } } - return onTouchEvent(event); + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); + } + return false; } /** @@ -4634,8 +4710,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,20 +4730,90 @@ 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}. * </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} 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} 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} 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; } /** @@ -4855,7 +5012,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility return; } Display d = WindowManagerImpl.getDefault().getDefaultDisplay(); - outRect.set(0, 0, d.getWidth(), d.getHeight()); + d.getRectSize(outRect); } /** @@ -5223,15 +5380,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} 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 +5463,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 || @@ -7212,8 +7438,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,8 +7553,9 @@ 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); @@ -7348,8 +7583,9 @@ 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); @@ -7371,8 +7607,9 @@ 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); @@ -7419,11 +7656,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 +7681,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 +7693,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); } } @@ -9867,12 +10106,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]; @@ -11540,6 +11785,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. diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 739758c..94eb429 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; @@ -156,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. */ @@ -185,6 +191,7 @@ public class ViewConfiguration { private final int mTouchSlop; private final int mPagingTouchSlop; private final int mDoubleTapSlop; + private final int mScaledTouchExplorationTapSlop; private final int mWindowTouchSlop; private final int mMaximumDrawingCacheSize; private final int mOverscrollDistance; @@ -206,6 +213,7 @@ public class ViewConfiguration { mTouchSlop = 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; @@ -242,6 +250,7 @@ public class ViewConfiguration { mTouchSlop = (int) (sizeAndDensity * 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 @@ -444,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..881cb76 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; @@ -141,8 +140,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> @@ -156,7 +171,7 @@ public class ViewDebug { public static boolean consistencyCheckEnabled = false; static { - if (Config.DEBUG) { + if (false) { Debug.setFieldsOn(ViewDebug.class, true); } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 8dc86ac..7b404b4 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -147,6 +147,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. * @@ -583,6 +586,35 @@ 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); + 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 && @@ -926,6 +958,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 +979,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 +1011,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 +1023,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 +1055,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 +1097,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 +1113,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 +1155,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 +1193,69 @@ 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. + 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 +1264,99 @@ 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. + 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 +1364,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 +1666,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. @@ -1931,11 +2130,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 +2179,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; @@ -3244,6 +3448,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 +3515,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 +3529,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 +3590,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 +3603,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; 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/ViewRoot.java b/core/java/android/view/ViewRoot.java index 7d6e18f..f02daba 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.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; @@ -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,14 +251,20 @@ 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) { try { InputMethodManager imm = InputMethodManager.getInstance(mainLooper); - sWindowSession = IWindowManager.Stub.asInterface( - ServiceManager.getService("window")) - .openSession(imm.getClient(), imm.getInputContext()); + sWindowSession = Display.getWindowManager().openSession( + imm.getClient(), imm.getInputContext()); mInitialized = true; } catch (RemoteException e) { } @@ -501,7 +508,12 @@ 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 // of launching apps and we don't want those to be hardware accelerated @@ -524,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; } } } @@ -661,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); } } @@ -1253,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 " @@ -1379,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) { @@ -1591,8 +1619,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; @@ -1669,7 +1709,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); } @@ -2001,8 +2041,24 @@ public final class ViewRoot extends Handler implements ViewParent, Debug.startMethodTracing("ViewRoot"); } + 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; @@ -2170,25 +2226,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; } /** @@ -2313,6 +2412,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); @@ -2329,7 +2440,7 @@ public final class ViewRoot extends Handler implements ViewParent, if (isDown) { ensureTouchMode(true); } - if(Config.LOGV) { + if(false) { captureMotionLog("captureDispatchPointer", event); } @@ -2407,7 +2518,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) { @@ -2417,8 +2528,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); @@ -2547,6 +2666,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; @@ -2782,6 +2909,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); @@ -2828,6 +2963,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); @@ -2840,7 +2979,7 @@ public final class ViewRoot extends Handler implements ViewParent, return; } - if (Config.LOGV) { + if (false) { captureKeyLog("captureDispatchKeyEvent", event); } @@ -2941,7 +3080,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); } } @@ -3232,16 +3371,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); } }; @@ -3388,6 +3530,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( diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index a4c4544..8336959 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( diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 334c68e..3d19380 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; @@ -394,6 +396,12 @@ public interface WindowManagerPolicy { LocalPowerManager powerManager); /** + * Called by window manager once it has the initial, default native + * display dimensions. + */ + public void setInitialDisplaySize(int width, int height); + + /** * Check permissions when adding a window. * * @param attrs The window's LayoutParams. @@ -808,6 +816,13 @@ public interface WindowManagerPolicy { boolean displayEnabled); /** + * Return the currently locked screen rotation, if any. Return + * Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180, or + * Surface.ROTATION_270 if locked; return -1 if not locked. + */ + public int getLockedRotationLw(); + + /** * Called when the system is mostly done booting to determine whether * the system should go into safe mode. */ 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..11c9392 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 int MAX_POOL_SIZE = 10; private static final Object mPoolLock = 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. * @@ -590,11 +393,11 @@ public final class AccessibilityEvent implements Parcelable { * <p> * <b>Note: You must not touch the object after calling this function.</b> */ + @Override public void recycle() { if (mIsInPool) { return; } - clear(); synchronized (mPoolLock) { if (sPoolSize <= MAX_POOL_SIZE) { @@ -609,44 +412,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 +431,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 +514,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..e095f43 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -0,0 +1,415 @@ +/* + * 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 mPoolLock = 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. + */ + protected static AccessibilityRecord obtain() { + synchronized (mPoolLock) { + 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> + */ + public void recycle() { + if (mIsInPool) { + return; + } + clear(); + synchronized (mPoolLock) { + 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..dd2d00d 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -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/InputConnection.java b/core/java/android/view/inputmethod/InputConnection.java index ea9e402..a6639d1 100644 --- a/core/java/android/view/inputmethod/InputConnection.java +++ b/core/java/android/view/inputmethod/InputConnection.java @@ -26,8 +26,9 @@ import android.view.KeyEvent; * is used to perform such things as reading text around the cursor, * committing text to the text box, and sending raw key events to the application. * - * <p>Implementations of this interface should generally be done by - * subclassing {@link BaseInputConnection}. + * <p>Applications should never directly implement this interface, but instead + * subclass from {@link BaseInputConnection}. This will ensure that the + * application does not break when new methods are added to the interface. */ public interface InputConnection { /** 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..eef2a33 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -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 { @@ -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/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index c7a7374..6306274 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -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/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java index 060c0bb..d1b8cfc 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -184,7 +184,9 @@ class HTML5VideoViewProxy extends Handler // we need to pause the old one and re-create a new media player // inside the HTML5VideoView. if (mHTML5VideoView != null) { - mHTML5VideoView.pauseAndDispatch(mCurrentProxy); + if (!backFromFullScreenMode) { + mHTML5VideoView.pauseAndDispatch(mCurrentProxy); + } // release the media player to avoid finalize error mHTML5VideoView.release(); } diff --git a/core/java/android/webkit/WebHistoryItem.java b/core/java/android/webkit/WebHistoryItem.java index ccf3d6b..7c0e478 100644 --- a/core/java/android/webkit/WebHistoryItem.java +++ b/core/java/android/webkit/WebHistoryItem.java @@ -90,9 +90,7 @@ public class WebHistoryItem implements Cloneable { * another item, the identifiers will be the same even if they are not the * same object. * @return The id for this item. - * @deprecated This method is now obsolete. */ - @Deprecated public int getId() { return mId; } diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 2b507fd..71d6080 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -43,10 +43,8 @@ public class WebSettings { * SINGLE_COLUMN moves all content into one column that is the width of the * view. * NARROW_COLUMNS makes all columns no wider than the screen if possible. - * @deprecated This enum is now obsolete. */ // XXX: These must match LayoutAlgorithm in Settings.h in WebCore. - @Deprecated public enum LayoutAlgorithm { NORMAL, SINGLE_COLUMN, @@ -512,18 +510,14 @@ public class WebSettings { /** * Enables dumping the pages navigation cache to a text file. - * @deprecated This method is now obsolete. */ - @Deprecated public void setNavDump(boolean enabled) { mNavDump = enabled; } /** * Returns true if dumping the navigation cache is enabled. - * @deprecated This method is now obsolete. */ - @Deprecated public boolean getNavDump() { return mNavDump; } @@ -661,9 +655,7 @@ public class WebSettings { * Set whether the WebView uses its background for over scroll background. * If true, it will use the WebView's background. If false, it will use an * internal pattern. Default is true. - * @deprecated This method is now obsolete. */ - @Deprecated public void setUseWebViewBackgroundForOverscrollBackground(boolean view) { mUseWebViewBackgroundForOverscroll = view; } @@ -671,9 +663,7 @@ public class WebSettings { /** * Returns true if this WebView uses WebView's background instead of * internal pattern for over scroll background. - * @deprecated This method is now obsolete. */ - @Deprecated public boolean getUseWebViewBackgroundForOverscrollBackground() { return mUseWebViewBackgroundForOverscroll; } @@ -876,9 +866,7 @@ public class WebSettings { * WebView. * @param l A LayoutAlgorithm enum specifying the algorithm to use. * @see WebSettings.LayoutAlgorithm - * @deprecated This method is now obsolete. */ - @Deprecated public synchronized void setLayoutAlgorithm(LayoutAlgorithm l) { // XXX: This will only be affective if libwebcore was built with // ANDROID_LAYOUT defined. @@ -893,9 +881,7 @@ public class WebSettings { * @return LayoutAlgorithm enum value describing the layout algorithm * being used. * @see WebSettings.LayoutAlgorithm - * @deprecated This method is now obsolete. */ - @Deprecated public synchronized LayoutAlgorithm getLayoutAlgorithm() { return mLayoutAlgorithm; } 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..b5d0492 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; @@ -853,17 +854,13 @@ public class WebView extends AbsoluteLayout private PictureListener mPictureListener; /** * Interface to listen for new pictures as they change. - * @deprecated This interface is now obsolete. */ - @Deprecated public interface PictureListener { /** * Notify the listener that the picture has changed. * @param view The WebView that owns the picture. * @param picture The new picture. - * @deprecated This method is now obsolete. */ - @Deprecated public void onNewPicture(WebView view, Picture picture); } @@ -874,8 +871,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 +892,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 @@ -1510,9 +1509,7 @@ public class WebView extends AbsoluteLayout /** * Enables platform notifications of data state and proxy changes. - * @deprecated Obsolete - platform notifications are always enabled. */ - @Deprecated public static void enablePlatformNotifications() { Network.enablePlatformNotifications(); } @@ -1520,9 +1517,7 @@ public class WebView extends AbsoluteLayout /** * If platform notifications are enabled, this should be called * from the Activity's onPause() or onStop(). - * @deprecated Obsolete - platform notifications are always enabled. */ - @Deprecated public static void disablePlatformNotifications() { Network.disablePlatformNotifications(); } @@ -1625,9 +1620,7 @@ public class WebView extends AbsoluteLayout * @param dest The file to store the serialized picture data. Will be * overwritten with this WebView's picture data. * @return True if the picture was successfully saved. - * @deprecated This method is now obsolete. */ - @Deprecated public boolean savePicture(Bundle b, final File dest) { if (dest == null || b == null) { return false; @@ -1689,9 +1682,7 @@ public class WebView extends AbsoluteLayout * @param b A Bundle containing the saved display data. * @param src The file where the picture data was stored. * @return True if the picture was successfully restored. - * @deprecated This method is now obsolete. */ - @Deprecated public boolean restorePicture(Bundle b, File src) { if (src == null || b == null) { return false; @@ -2393,6 +2384,8 @@ public class WebView extends AbsoluteLayout */ public void setTitleBarGravity(int gravity) { mTitleGravity = gravity; + // force refresh + invalidate(); } /** @@ -3634,9 +3627,7 @@ public class WebView extends AbsoluteLayout * Set the Picture listener. This is an interface used to receive * notifications of a new Picture. * @param listener An implementation of WebView.PictureListener. - * @deprecated This method is now obsolete. */ - @Deprecated public void setPictureListener(PictureListener listener) { mPictureListener = listener; } @@ -4995,9 +4986,7 @@ public class WebView extends AbsoluteLayout /** * Use this method to put the WebView into text selection mode. * Do not rely on this functionality; it will be deprecated in the future. - * @deprecated This method is now obsolete. */ - @Deprecated public void emulateShiftHeld() { setUpSelect(false, 0, 0); } @@ -7865,7 +7854,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 @@ -8689,10 +8681,6 @@ public class WebView extends AbsoluteLayout mWebViewCore.sendMessage(EventHub.SET_BACKGROUND_COLOR, color); } - /** - * @deprecated This method is now obsolete. - */ - @Deprecated public void debugDump() { nativeDebugDump(); mWebViewCore.sendMessage(EventHub.DUMP_NAVTREE); @@ -8760,7 +8748,7 @@ public class WebView extends AbsoluteLayout 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/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..618820d --- /dev/null +++ b/core/java/android/webkit/webdriver/WebDriver.java @@ -0,0 +1,551 @@ +/* + * 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.os.Handler; +import android.os.Message; +import android.webkit.WebView; + +import com.android.internal.R; + +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; + +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; + + // Commands posted to the handler + private static final int CMD_GET_URL = 1; + private static final int CMD_EXECUTE_SCRIPT = 2; + + 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; + + // Updated when the command is done executing in the main thread. + private volatile boolean mCommandDone; + + private WebView mWebView; + + // 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) { + if (msg.what == CMD_GET_URL) { + final String url = (String) msg.obj; + mWebView.loadUrl(url); + } else if (msg.what == CMD_EXECUTE_SCRIPT) { + mWebView.loadUrl("javascript:" + (String) msg.obj); + } + } + }; + + /** + * 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]; + } + } + throw new IllegalArgumentException(intValue + + " does not map to any ErrorCode."); + } + } + + public WebDriver(WebView webview) { + this.mWebView = webview; + 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(); + this.mWebView = webview; + WebchromeClientWrapper chromeWrapper = new WebchromeClientWrapper( + webview.getWebChromeClient(), this); + mWebView.setWebChromeClient(chromeWrapper); + mWebView.addJavascriptInterface(new JavascriptResultReady(), + "webdriver"); + } + + /** + * 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) { + executeCommand(CMD_GET_URL, url, LOADING_TIMEOUT); + mDocumentElement = (WebElement) executeScript("return document.body;"); + } + + /** + * @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. + */ + public void quit() { + mWebView.clearCache(true); + mWebView.clearFormData(); + mWebView.clearHistory(); + mWebView.clearSslPreferences(); + mWebView.clearView(); + } + + /** + * 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)"); + } + + /** + * 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) { + String result = executeCommand(CMD_EXECUTE_SCRIPT, + "window.webdriver.resultReady(" + script + ")", + JS_EXECUTION_TIMEOUT); + 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(); + } + + /** + * 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."); + } + 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..96700d7 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebElement.java @@ -0,0 +1,255 @@ +/* + * 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 com.android.internal.R; + +import java.util.List; + +/** + * 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); + } + + /*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); + 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 = (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/WebchromeClientWrapper.java b/core/java/android/webkit/webdriver/WebchromeClientWrapper.java new file mode 100644 index 0000000..ea33c5b --- /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.notifyCommandDone(); + } + 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..d63d421 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; @@ -2556,6 +2557,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; } @@ -4529,8 +4541,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; } 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/AdapterView.java b/core/java/android/widget/AdapterView.java index f16efbd..060f1a9 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,27 @@ 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 onPopulateAccessibilityEvent(AccessibilityEvent 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..bd595a5 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 onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + event.setChecked(mChecked); } } diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index 0df45cc..f050d41 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -208,22 +208,18 @@ 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); + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + + int resourceId = 0; + if (mChecked) { + resourceId = R.string.accessibility_compound_button_selected; + } else { + resourceId = R.string.accessibility_compound_button_unselected; } - - return populated; + String state = getResources().getString(resourceId); + event.getText().add(state); + 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/FrameLayout.java b/core/java/android/widget/FrameLayout.java index f659ead..590a768 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 @@ -566,4 +566,3 @@ public class FrameLayout extends ViewGroup { } } } - diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index fd0e53d..dbe9288 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -230,6 +230,30 @@ 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; + } + @Override protected void onDraw(Canvas canvas) { if (mDivider == null) { @@ -244,29 +268,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 +286,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 +310,7 @@ public class LinearLayout extends ViewGroup { } } - if (showDividerEnd) { + if (hasDividerBeforeChildAt(count)) { drawVerticalDivider(canvas, left); } } @@ -523,6 +519,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 +567,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 +581,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 +678,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 +882,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 +896,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 +1011,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 +1347,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 +1381,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 +1440,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 +1498,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..5618dbe 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); @@ -1982,36 +1998,32 @@ public class ListView extends AbsListView { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = super.dispatchPopulateAccessibilityEvent(event); + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(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--; - } + 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; } + } else { + itemCount = count; } - - event.setItemCount(itemCount); - event.setCurrentItemIndex(currentItemIndex); } - return populated; + event.setItemCount(itemCount); + event.setCurrentItemIndex(currentItemIndex); } /** diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index cf72ec4..96d41a0 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> @@ -125,6 +127,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; @@ -156,6 +159,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 @@ -542,8 +547,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) { @@ -1007,8 +1015,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 onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(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/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/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..31ec785 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 onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(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..4d3aa68 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -82,10 +82,12 @@ import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; import android.text.style.ClickableSpan; import android.text.style.ParagraphStyle; +import android.text.style.SuggestionSpan; import android.text.style.URLSpan; 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; @@ -309,6 +311,10 @@ 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 int mCursorDrawableRes; private final Drawable[] mCursorDrawable = new Drawable[2]; private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2 @@ -777,6 +783,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 +2967,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; @@ -3977,13 +4005,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 +4231,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 +4308,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 +4570,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 +5248,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 +5435,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 +5443,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 +6290,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 +6771,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. + * + * 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 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>. + * @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 +6797,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 } /** @@ -7200,14 +7213,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() { @@ -7326,9 +7331,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 +7347,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(); } } } @@ -7797,7 +7793,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // 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; @@ -7900,9 +7896,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { if (!isShown()) { - return false; + return; } final boolean isPassword = hasPasswordTransformationMethod(); @@ -7913,15 +7909,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener 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; } void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, @@ -8045,7 +8037,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(); } @@ -8180,7 +8172,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int offset = getOffset(mLastDownPositionX, mLastDownPositionY); stopSelectionActionMode(); Selection.setSelection((Spannable)mText, offset); - getInsertionController().show(0); + getInsertionController().showWithPaste(); handled = true; } @@ -8195,8 +8187,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 +8203,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 +8225,201 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); } + private class SuggestionsPopupWindow implements OnClickListener { + private static final int MAX_NUMBER_SUGGESTIONS = 5; + private static final long NO_SUGGESTIONS = -1L; + private final PopupWindow mContainer; + private final ViewGroup[] mSuggestionViews = new ViewGroup[2]; + private final int[] mSuggestionViewLayouts = new int[] { + mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout}; + + 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 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); + } + + 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; + for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { + SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; + final int spanStart = spannable.getSpanStart(suggestionSpan); + final int spanEnd = spannable.getSpanEnd(suggestionSpan); + final Long spanRange = packRangeInLong(spanStart, spanEnd); + + 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]); + textView.setTag(spanRange); + + totalNbSuggestions++; + if (totalNbSuggestions == MAX_NUMBER_SUGGESTIONS) { + 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"); + textView.setTag(NO_SUGGESTIONS); + totalNbSuggestions++; + } + + 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(); + } + + public void hide() { + mContainer.dismiss(); + } + + @Override + public void onClick(View view) { + if (view instanceof TextView) { + TextView textView = (TextView) view; + Long range = ((Long) view.getTag()); + if (range != NO_SUGGESTIONS) { + final int spanStart = extractRangeStartFromLong(range); + final int spanEnd = extractRangeEndFromLong(range); + ((Editable) mText).replace(spanStart, spanEnd, textView.getText()); + } + } + 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. @@ -8453,65 +8628,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 +8718,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 +8757,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 +8834,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 +8858,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 +8891,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 +8915,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 +8973,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 +8987,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 +9019,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 +9039,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 +9077,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(); + + /** + * 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(); + } - getHandle().positionAtCursor(offset); + 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 void showWithPaste() { + ((InsertionHandleView) getHandle()).show(0); } - public int getCurrentOffset(HandleView handle) { - return getSelectionStart(); + public void hide() { + if (mHandle != null) { + mHandle.hide(); + } } public boolean onTouchEvent(MotionEvent ev) { @@ -9145,30 +9341,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 +9376,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 +9411,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 +9479,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 +9501,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/com/android/internal/app/ActionBarImpl.java b/core/java/com/android/internal/app/ActionBarImpl.java index 8f1354b..dccfa6c 100644 --- a/core/java/com/android/internal/app/ActionBarImpl.java +++ b/core/java/com/android/internal/app/ActionBarImpl.java @@ -25,13 +25,13 @@ 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,9 +42,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.HorizontalScrollView; import android.widget.LinearLayout; import android.widget.SpinnerAdapter; @@ -59,6 +60,7 @@ import java.util.ArrayList; * which is normally hidden. */ public class ActionBarImpl extends ActionBar { + private static final String TAG = "ActionBarImpl"; private static final int NORMAL_VIEW = 0; private static final int CONTEXT_VIEW = 1; @@ -71,6 +73,7 @@ public class ActionBarImpl extends ActionBar { private ActionBarContextView mUpperContextView; private LinearLayout mLowerContextView; private View mContentView; + private ViewGroup mExternalTabView; private ArrayList<TabImpl> mTabs = new ArrayList<TabImpl>(); @@ -92,60 +95,34 @@ 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) { - } - + new AnimatorListenerAdapter() { // NORMAL_VIEW @Override public void onAnimationEnd(Animator animation) { if (mLowerContextView != null) { mLowerContextView.removeAllViews(); } - mCurrentAnim = null; + mCurrentModeAnim = 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) { - } - + new AnimatorListenerAdapter() { // CONTEXT_VIEW @Override public void onAnimationEnd(Animator animation) { - mCurrentAnim = null; + mCurrentModeAnim = 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 +130,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) { @@ -218,6 +175,18 @@ public class ActionBarImpl extends ActionBar { mActionView.setContextView(mUpperContextView); mContextDisplayMode = mLowerContextView == null ? CONTEXT_DISPLAY_NORMAL : CONTEXT_DISPLAY_SPLIT; + + 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 +198,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 +254,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)); } @@ -370,6 +344,7 @@ public class ActionBarImpl extends ActionBar { mUpperContextView.killMode(); ActionMode mode = new ActionModeImpl(callback); if (callback.onCreateActionMode(mode, mode.getMenu())) { + mWasHiddenBeforeMode = !isShowing(); mode.invalidate(); mUpperContextView.initForMode(mode); animateTo(CONTEXT_VIEW); @@ -378,7 +353,6 @@ public class ActionBarImpl extends ActionBar { mLowerContextView.setVisibility(View.VISIBLE); } mActionMode = mode; - show(); return mode; } return null; @@ -498,10 +472,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); @@ -517,17 +496,19 @@ public class ActionBarImpl extends ActionBar { b.with(ObjectAnimator.ofFloat(mContainerView, "translationY", 0)); } 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; @@ -545,7 +526,7 @@ public class ActionBarImpl extends ActionBar { -mContainerView.getHeight())); } anim.addListener(mHideListener); - mCurrentAnim = anim; + mCurrentShowAnim = anim; anim.start(); } else { mHideListener.onAnimationEnd(null); @@ -556,19 +537,23 @@ public class ActionBarImpl extends ActionBar { return mContainerView.getVisibility() == View.VISIBLE; } - private long animateTo(int viewIndex) { - show(); + long animateTo(int viewIndex) { + show(false); + if (mCurrentModeAnim != null) { + mCurrentModeAnim.end(); + } AnimatorSet set = new AnimatorSet(); final View targetChild = mContainerView.getChildAt(viewIndex); targetChild.setVisibility(View.VISIBLE); + targetChild.setAlpha(0); 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) { + if (i == viewIndex || child == mContainerView.getTabContainer()) { continue; } @@ -581,7 +566,7 @@ public class ActionBarImpl extends ActionBar { set.addListener(mAfterAnimation[viewIndex]); - mCurrentAnim = set; + mCurrentModeAnim = set; set.start(); return set.getDuration(); } @@ -636,6 +621,10 @@ public class ActionBarImpl extends ActionBar { mLowerContextView.setVisibility(View.GONE); } mActionMode = null; + + if (mWasHiddenBeforeMode) { + hide(); + } } @Override @@ -718,7 +707,7 @@ public class ActionBarImpl extends ActionBar { return; } invalidate(); - mUpperContextView.openOverflowMenu(); + mUpperContextView.showOverflowMenu(); } } @@ -871,11 +860,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 +884,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 d86504d..7cf33fc 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 ((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 ((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/util/AsyncChannel.java b/core/java/com/android/internal/util/AsyncChannel.java index 101dd91..4d656c0 100644 --- a/core/java/com/android/internal/util/AsyncChannel.java +++ b/core/java/com/android/internal/util/AsyncChannel.java @@ -135,6 +135,8 @@ public class AsyncChannel { * channel is forcibly disconnected by the system or as a reply to CMD_CHANNEL_DISCONNECT. * * msg.arg1 == 0 : STATUS_SUCCESSFUL + * 1 : STATUS_BINDING_UNSUCCESSFUL + * 2 : STATUS_SEND_UNSUCCESSFUL * : All other values signify failure and the channel state is indeterminate * msg.obj == the AsyncChannel * msg.replyTo = messenger disconnecting or null if it was never connected. @@ -147,6 +149,9 @@ public class AsyncChannel { /** Error attempting to bind on a connect */ public static final int STATUS_BINDING_UNSUCCESSFUL = 1; + /** Error attempting to send a message */ + public static final int STATUS_SEND_UNSUCCESSFUL = 2; + /** Service connection */ private AsyncChannelConnection mConnection; @@ -345,11 +350,7 @@ public class AsyncChannel { mSrcContext.unbindService(mConnection); } if (mSrcHandler != null) { - Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_DISCONNECTED); - msg.arg1 = STATUS_SUCCESSFUL; - msg.obj = this; - msg.replyTo = mDstMessenger; - mSrcHandler.sendMessage(msg); + replyDisconnected(STATUS_SUCCESSFUL); } } @@ -363,7 +364,7 @@ public class AsyncChannel { try { mDstMessenger.send(msg); } catch (RemoteException e) { - log("TODO: handle sendMessage RemoteException" + e); + replyDisconnected(STATUS_SEND_UNSUCCESSFUL); } } @@ -712,6 +713,7 @@ public class AsyncChannel { /** * Reply to the src handler that we're half connected. + * see: CMD_CHANNEL_HALF_CONNECTED for message contents * * @param status to be stored in msg.arg1 */ @@ -724,6 +726,21 @@ public class AsyncChannel { } /** + * Reply to the src handler that we are disconnected + * see: CMD_CHANNEL_DISCONNECTED for message contents + * + * @param status to be stored in msg.arg1 + */ + private void replyDisconnected(int status) { + Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_DISCONNECTED); + msg.arg1 = status; + msg.obj = this; + msg.replyTo = mDstMessenger; + mSrcHandler.sendMessage(msg); + } + + + /** * ServiceConnection to receive call backs. */ class AsyncChannelConnection implements ServiceConnection { @@ -736,11 +753,7 @@ public class AsyncChannel { } public void onServiceDisconnected(ComponentName className) { - Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_DISCONNECTED); - msg.arg1 = STATUS_SUCCESSFUL; - msg.obj = AsyncChannel.this; - msg.replyTo = mDstMessenger; - mSrcHandler.sendMessage(msg); + replyDisconnected(STATUS_SUCCESSFUL); } } 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..a05fa53 --- /dev/null +++ b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java @@ -0,0 +1,441 @@ +/* + * 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.Log; +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 View mOverflowButton; + private boolean mReserveOverflow; + private int mWidthLimit; + private int mActionItemWidthLimit; + private int mMaxItems; + + // 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) { + OverflowMenuButton button = new OverflowMenuButton(mContext); + mOverflowButton = button; + 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; + } + + @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); + } + 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) { + Log.d("ActionMenuPresenter", "showOverflowMenu"); + 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; + } + + // 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); + } + } + } + + 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 7775f00..0ea9c89 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuView.java +++ b/core/java/com/android/internal/view/menu/ActionMenuView.java @@ -17,63 +17,22 @@ 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.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; public ActionMenuView(Context context) { this(context, null); @@ -81,60 +40,28 @@ 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 int screen = res.getConfiguration().screenLayout; - mReserveOverflow = (screen & Configuration.SCREENLAYOUT_SIZE_MASK) == - Configuration.SCREENLAYOUT_SIZE_XLARGE; - 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); - final int screen = newConfig.screenLayout; - mReserveOverflow = (screen & Configuration.SCREENLAYOUT_SIZE_MASK) == - Configuration.SCREENLAYOUT_SIZE_XLARGE; - mMaxItems = getMaxActionButtons(); - mWidthLimit = getResources().getDisplayMetrics().widthPixels / 2; - if (mMenu != null) { - mMenu.setMaxActionItems(mMaxItems); - updateChildren(false); - } + mPresenter.updateMenuView(false); - if (mOverflowPopup != null && mOverflowPopup.isShowing()) { - mOverflowPopup.dismiss(); - post(mShowOverflow); + if (mPresenter != null && mPresenter.isOverflowMenuShowing()) { + mPresenter.hideOverflowMenu(); + mPresenter.showOverflowMenu(); } } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); - if (mOverflowPopup != null && mOverflowPopup.isShowing()) { - mOverflowPopup.dismiss(); - } - removeCallbacks(mShowOverflow); - if (mPostedOpenRunnable != null) { - removeCallbacks(mPostedOpenRunnable); - } - } - - private int getMaxActionButtons() { - return getResources().getInteger(com.android.internal.R.integer.max_action_buttons); + mPresenter.dismissPopupMenus(); } public boolean isOverflowReserved() { @@ -144,10 +71,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() { @@ -169,6 +92,11 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo return generateDefaultLayoutParams(); } + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + public boolean invokeItem(MenuItemImpl item) { return mMenu.performItemAction(item, 0); } @@ -177,243 +105,26 @@ 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; - } - - MenuPopupHelper popup = mOverflowPopup; - if (popup != null) { - popup.dismiss(); - 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(); } - return false; - } - - private boolean addItemView(boolean needsDivider, ActionMenuItemView view) { - view.setItemInvoker(this); - boolean hasText = view.hasText(); - - if (hasText && needsDivider) { - addView(makeDividerView(), makeDividerLayoutParams()); + if (childIndex > 0 && child instanceof ActionMenuChildView) { + result |= ((ActionMenuChildView) child).needsDividerBefore(); } - 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()); - } - - private class OverflowMenuButton extends ImageButton { - 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; - } - } - - 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.getCallback().onCloseMenu(mMenu, true); - mOverflowPopup = null; - } + public interface ActionMenuChildView { + public boolean needsDividerBefore(); + public boolean needsDividerAfter(); } } 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..7fba5ca 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,66 @@ 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; + + 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(); } - /** - * 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 +271,7 @@ public class MenuBuilder implements Menu { } mItems.add(findInsertIndex(mItems, ordering), item); - onItemsChanged(false); + onItemsChanged(true); return item; } @@ -554,7 +357,7 @@ public class MenuBuilder implements Menu { } // Notify menu views - onItemsChanged(false); + onItemsChanged(true); } } @@ -573,7 +376,7 @@ public class MenuBuilder implements Menu { mItems.remove(index); - if (updateChildrenOnMenuViews) onItemsChanged(false); + if (updateChildrenOnMenuViews) onItemsChanged(true); } public void removeItemAt(int index) { @@ -585,6 +388,7 @@ public class MenuBuilder implements Menu { clear(); clearHeader(); mPreventDispatchingItemsChanged = false; + mItemsChangedWhileDispatchPrevented = false; onItemsChanged(true); } @@ -725,19 +529,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 +550,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 +568,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 +579,7 @@ public class MenuBuilder implements Menu { if (mShortcutsVisible == shortcutsVisible) return; setShortcutsVisibleInner(shortcutsVisible); - refreshShortcuts(mShortcutsVisible, isQwertyMode()); + onItemsChanged(false); } private void setShortcutsVisibleInner(boolean shortcutsVisible) { @@ -818,15 +599,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 +650,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 +658,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 +679,6 @@ public class MenuBuilder implements Menu { items.add(item); } } - return items; } /* @@ -908,9 +694,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 +708,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 +749,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 +770,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 +792,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 +835,7 @@ public class MenuBuilder implements Menu { */ void onItemVisibleChanged(MenuItemImpl item) { // Notify of items being changed + mIsVisibleItemsStale = true; onItemsChanged(false); } @@ -1034,6 +845,7 @@ public class MenuBuilder implements Menu { */ void onItemActionRequestChanged(MenuItemImpl item) { // Notify of items being changed + mIsActionItemsStale = true; onItemsChanged(false); } @@ -1055,17 +867,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 +891,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 +1072,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 +1079,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..6387c9b 100644 --- a/core/java/com/android/internal/view/menu/MenuDialogHelper.java +++ b/core/java/com/android/internal/view/menu/MenuDialogHelper.java @@ -24,17 +24,19 @@ 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; public MenuDialogHelper(MenuBuilder menu) { mMenu = menu; @@ -49,12 +51,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 +73,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; @@ -132,9 +134,25 @@ 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(); + } + } + + @Override + public boolean onOpenSubMenu(MenuBuilder 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/ActionBarContainer.java b/core/java/com/android/internal/widget/ActionBarContainer.java index c9b0ec9..3deb036 100644 --- a/core/java/com/android/internal/widget/ActionBarContainer.java +++ b/core/java/com/android/internal/widget/ActionBarContainer.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.MotionEvent; +import android.view.View; import android.widget.FrameLayout; /** @@ -29,6 +30,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 +67,44 @@ 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 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 71af115..70fb3b2 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; @@ -53,6 +54,7 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener private int mTitleStyleRes; private int mSubtitleStyleRes; private ActionMenuView mMenuView; + private ActionMenuPresenter mPresenter; private Animator mCurrentAnimation; private boolean mAnimateInOnLayout; @@ -176,9 +178,9 @@ 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); + mPresenter = new ActionMenuPresenter(); + menu.addMenuPresenter(mPresenter); + mMenuView = (ActionMenuView) mPresenter.getMenuView(this); addView(mMenuView); mAnimateInOnLayout = true; @@ -217,28 +219,22 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener } public boolean showOverflowMenu() { - if (mMenuView != null) { - return mMenuView.showOverflowMenu(); + if (mPresenter != null) { + return mPresenter.showOverflowMenu(); } return false; } - public void openOverflowMenu() { - if (mMenuView != null) { - mMenuView.openOverflowMenu(); - } - } - public boolean hideOverflowMenu() { - if (mMenuView != null) { - return mMenuView.hideOverflowMenu(); + if (mPresenter != null) { + return mPresenter.hideOverflowMenu(); } return false; } public boolean isOverflowMenuShowing() { - if (mMenuView != null) { - return mMenuView.isOverflowMenuShowing(); + if (mPresenter != null) { + return mPresenter.isOverflowMenuShowing(); } return false; } diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index 81d02ee..a572e11 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,11 +30,13 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +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; @@ -83,7 +87,6 @@ public class ActionBarView extends ViewGroup { private CharSequence mSubtitle; private Drawable mIcon; private Drawable mLogo; - private Drawable mDivider; private View mHomeLayout; private View mHomeAsUpView; @@ -94,7 +97,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,9 +112,11 @@ public class ActionBarView extends ViewGroup { private boolean mShowMenu; private boolean mUserTitle; + private boolean mIncludeTabs; private MenuBuilder mOptionsMenu; private ActionMenuView mMenuView; + private ActionMenuPresenter mActionMenuPresenter; private ActionBarContextView mContextView; @@ -197,6 +202,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); @@ -208,8 +215,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); @@ -240,6 +245,14 @@ public class ActionBarView extends ViewGroup { addView(mIndeterminateProgressView); } + public boolean hasEmbeddedTabs() { + return mIncludeTabs; + } + + public void setExternalTabLayout(ViewGroup tabLayout) { + mTabLayout = tabLayout; + } + @Override public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) { // No starting an action mode for an action bar child! (Where would it go?) @@ -250,16 +263,24 @@ public class ActionBarView extends ViewGroup { mCallback = callback; } - public void setMenu(Menu menu) { + public void setMenu(Menu menu, MenuPresenter.Callback cb) { if (menu == mOptionsMenu) return; + if (mOptionsMenu != null) { + mOptionsMenu.removeMenuPresenter(mActionMenuPresenter); + } + MenuBuilder builder = (MenuBuilder) menu; mOptionsMenu = builder; if (mMenuView != null) { removeView(mMenuView); } - final ActionMenuView menuView = (ActionMenuView) builder.getMenuView( - MenuBuilder.TYPE_ACTION_BUTTON, null); + if (mActionMenuPresenter == null) { + mActionMenuPresenter = new ActionMenuPresenter(); + mActionMenuPresenter.setCallback(cb); + builder.addMenuPresenter(mActionMenuPresenter); + } + final ActionMenuView menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this); final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); menuView.setLayoutParams(layoutParams); @@ -268,15 +289,15 @@ public class ActionBarView extends ViewGroup { } public boolean showOverflowMenu() { - if (mMenuView != null) { - return mMenuView.showOverflowMenu(); + if (mActionMenuPresenter != null) { + return mActionMenuPresenter.showOverflowMenu(); } return false; } public void openOverflowMenu() { - if (mMenuView != null) { - mMenuView.openOverflowMenu(); + if (mActionMenuPresenter != null) { + showOverflowMenu(); } } @@ -289,28 +310,27 @@ public class ActionBarView extends ViewGroup { } public boolean hideOverflowMenu() { - if (mMenuView != null) { - return mMenuView.hideOverflowMenu(); + if (mActionMenuPresenter != null) { + return mActionMenuPresenter.hideOverflowMenu(); } return false; } public boolean isOverflowMenuShowing() { - if (mMenuView != null) { - return mMenuView.isOverflowMenuShowing(); + if (mActionMenuPresenter != null) { + return mActionMenuPresenter.isOverflowMenuShowing(); } return false; } - public boolean isOverflowMenuOpen() { - if (mMenuView != null) { - return mMenuView.isOverflowMenuOpen(); - } - return false; + public boolean isOverflowReserved() { + return mActionMenuPresenter != null && mActionMenuPresenter.isOverflowReserved(); } - public boolean isOverflowReserved() { - return mMenuView != null && mMenuView.isOverflowReserved(); + public void dismissPopupMenus() { + if (mActionMenuPresenter != null) { + mActionMenuPresenter.dismissPopupMenus(); + } } public void setCustomNavigationView(View view) { @@ -380,6 +400,12 @@ 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); @@ -416,6 +442,48 @@ public class ActionBarView extends ViewGroup { } } + 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) { final int oldMode = mNavigationMode; if (mode != oldMode) { @@ -426,7 +494,7 @@ public class ActionBarView extends ViewGroup { } break; case ActionBar.NAVIGATION_MODE_TABS: - if (mTabLayout != null) { + if (mTabScrollView != null) { removeView(mTabScrollView); } } @@ -451,7 +519,9 @@ public class ActionBarView extends ViewGroup { break; case ActionBar.NAVIGATION_MODE_TABS: ensureTabsExist(); - addView(mTabScrollView); + if (mTabScrollView != null) { + addView(mTabScrollView); + } break; } mNavigationMode = mode; @@ -460,15 +530,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) { 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/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java index 076a1cb..c34cb9e 100644 --- a/core/java/com/android/internal/widget/PointerLocationView.java +++ b/core/java/com/android/internal/widget/PointerLocationView.java @@ -357,6 +357,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; 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; |