diff options
Diffstat (limited to 'core/java')
379 files changed, 32181 insertions, 8798 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 03346fe..8bb305d 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -25,6 +25,7 @@ import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; /** * An accessibility service runs in the background and receives callbacks by the system @@ -39,21 +40,67 @@ import android.view.accessibility.AccessibilityEvent; * <p> * <code> * <service android:name=".MyAccessibilityService"><br> - * <intent-filter><br> - * <action android:name="android.accessibilityservice.AccessibilityService" /><br> - * </intent-filter><br> + * <intent-filter><br> + * <action android:name="android.accessibilityservice.AccessibilityService" /><br> + * </intent-filter><br> * </service><br> * </code> + * </p> * <p> * The lifecycle of an accessibility service is managed exclusively by the system. Starting * or stopping an accessibility service is triggered by an explicit user action through * enabling or disabling it in the device settings. After the system binds to a service it * calls {@link AccessibilityService#onServiceConnected()}. This method can be - * overriden by clients that want to perform post binding setup. An accessibility service - * is configured though setting an {@link AccessibilityServiceInfo} by calling - * {@link AccessibilityService#setServiceInfo(AccessibilityServiceInfo)}. You can call this - * method any time to change the service configuration but it is good practice to do that - * in the overriden {@link AccessibilityService#onServiceConnected()}. + * overriden by clients that want to perform post binding setup. + * </p> + * <p> + * An accessibility service can be configured to receive specific types of accessibility events, + * listen only to specific packages, get events from each type only once in a given time frame, + * retrieve window content, specify a settings activity, etc. + * </p> + * There are two approaches for configuring an accessibility service: + * <ul> + * <li> + * Providing a {@link #SERVICE_META_DATA meta-data} entry in the manifest when declaring + * the service. A service declaration with a meta-data tag is presented below: + * <p> + * <code> + * <service android:name=".MyAccessibilityService"><br> + * <intent-filter><br> + * <action android:name="android.accessibilityservice.AccessibilityService" /><br> + * </intent-filter><br> + * <meta-data android:name="android.accessibilityservice.as" android:resource="@xml/accessibilityservice" /><br> + * </service><br> + * </code> + * </p> + * <p> + * <strong> + * This approach enables setting all accessibility service properties. + * </strong> + * </p> + * <p> + * For more details refer to {@link #SERVICE_META_DATA}. + * </p> + * </li> + * <li> + * Calling {@link AccessibilityService#setServiceInfo(AccessibilityServiceInfo)}. Note + * that this method can be called any time to change the service configuration.<br> + * <p> + * <strong> + * This approach enables setting only dynamically configurable accessibility + * service properties: + * {@link AccessibilityServiceInfo#eventTypes}, + * {@link AccessibilityServiceInfo#feedbackType}, + * {@link AccessibilityServiceInfo#flags}, + * {@link AccessibilityServiceInfo#notificationTimeout}, + * {@link AccessibilityServiceInfo#packageNames} + * </strong> + * </p> + * <p> + * For more details refer to {@link AccessibilityServiceInfo}. + * </p> + * </li> + * </ul> * <p> * An accessibility service can be registered for events in specific packages to provide a * specific type of feedback and is notified with a certain timeout after the last event @@ -105,6 +152,62 @@ public abstract class AccessibilityService extends Service { public static final String SERVICE_INTERFACE = "android.accessibilityservice.AccessibilityService"; + /** + * Name under which an AccessibilityService component publishes information + * about itself. This meta-data must reference an XML resource containing + * an + * <code><{@link android.R.styleable#AccessibilityService accessibility-service}></code> + * tag. This is a a sample XML file configuring an accessibility service: + * <p> + * <code> + * <?xml version="1.0" encoding="utf-8"?><br> + * <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"<br> + * android:accessibilityEventTypes="typeViewClicked|typeViewFocused"<br> + * android:packageNames="foo.bar, foo.baz"<br> + * android:accessibilityFeedbackType="feedbackSpoken"<br> + * android:notificationTimeout="100"<br> + * android:accessibilityFlags="flagDefault"<br> + * android:settingsActivity="foo.bar.TestBackActivity"<br> + * . . .<br> + * /> + * </code> + * </p> + * <p> + * <strong>Note:</strong> A service can retrieve only the content of the active window. + * An active window is the source of the most recent event of type + * {@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_START}, + * {@link AccessibilityEvent#TYPE_TOUCH_EXPLORATION_GESTURE_END}, + * {@link AccessibilityEvent#TYPE_VIEW_CLICKED}, + * {@link AccessibilityEvent#TYPE_VIEW_FOCUSED}, + * {@link AccessibilityEvent#TYPE_VIEW_HOVER_ENTER}, + * {@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT}, + * {@link AccessibilityEvent#TYPE_VIEW_LONG_CLICKED}, + * {@link AccessibilityEvent#TYPE_VIEW_SELECTED}, + * {@link AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED}, + * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED}. + * Therefore the service should: + * <ul> + * <li> + * Register for all event types with no notification timeout and keep track + * for the active window by calling + * {@link AccessibilityEvent#getAccessibilityWindowId()} of the last received + * event and compare this with the + * {@link AccessibilityNodeInfo#getAccessibilityWindowId()} before calling + * retrieval methods on the latter. + * </li> + * <li> + * Prepare that a retrieval method on {@link AccessibilityNodeInfo} may fail + * since the active window has changed and the service did not get the + * accessibility event. Note that it is possible to have a retrieval method + * failing event adopting the strategy specified in the previous bullet + * because the accessibility event dispatch is asynchronous and crosses + * process boundaries. + * </li> + * <ul> + * </p> + */ + public static final String SERVICE_META_DATA = "android.accessibilityservice"; + private static final String LOG_TAG = "AccessibilityService"; private AccessibilityServiceInfo mInfo; @@ -165,7 +268,7 @@ public abstract class AccessibilityService extends Service { /** * Implement to return the implementation of the internal accessibility - * service interface. Subclasses should not override. + * service interface. */ @Override public final IBinder onBind(Intent intent) { diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index bf9e07d..b9878cd 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -16,8 +16,25 @@ package android.accessibilityservice; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; import android.os.Parcel; import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Xml; +import android.view.accessibility.AccessibilityEvent; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; /** * This class describes an {@link AccessibilityService}. The system @@ -30,6 +47,8 @@ import android.os.Parcelable; */ public class AccessibilityServiceInfo implements Parcelable { + private static final String TAG_ACCESSIBILITY_SERVICE = "accessibility-service"; + /** * Denotes spoken feedback. */ @@ -64,7 +83,9 @@ public class AccessibilityServiceInfo implements Parcelable { /** * The event types an {@link AccessibilityService} is interested in. - * + * <p> + * <strong>Can be dynamically set at runtime.</strong> + * </p> * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_CLICKED * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_LONG_CLICKED * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_FOCUSED @@ -77,13 +98,18 @@ public class AccessibilityServiceInfo implements Parcelable { /** * The package names an {@link AccessibilityService} is interested in. Setting - * to null is equivalent to all packages. + * to null is equivalent to all packages. + * <p> + * <strong>Can be dynamically set at runtime.</strong> + * </p> */ public String[] packageNames; /** * The feedback type an {@link AccessibilityService} provides. - * + * <p> + * <strong>Can be dynamically set at runtime.</strong> + * </p> * @see #FEEDBACK_AUDIBLE * @see #FEEDBACK_GENERIC * @see #FEEDBACK_HAPTIC @@ -96,6 +122,9 @@ public class AccessibilityServiceInfo implements Parcelable { * The timeout after the most recent event of a given type before an * {@link AccessibilityService} is notified. * <p> + * <strong>Can be dynamically set at runtime.</strong>. + * </p> + * <p> * Note: The event notification timeout is useful to avoid propagating events to the client * too frequently since this is accomplished via an expensive interprocess call. * One can think of the timeout as a criteria to determine when event generation has @@ -106,11 +135,181 @@ public class AccessibilityServiceInfo implements Parcelable { /** * This field represents a set of flags used for configuring an * {@link AccessibilityService}. - * + * <p> + * <strong>Can be dynamically set at runtime.</strong> + * </p> * @see #DEFAULT */ public int flags; + /** + * The unique string Id to identify the accessibility service. + */ + private String mId; + + /** + * The Service that implements this accessibility service component. + */ + private ResolveInfo mResolveInfo; + + /** + * The accessibility service setting activity's name, used by the system + * settings to launch the setting activity of this accessibility service. + */ + private String mSettingsActivityName; + + /** + * Flag whether this accessibility service can retrieve screen content. + */ + private boolean mCanRetrieveWindowContent; + + /** + * Creates a new instance. + */ + public AccessibilityServiceInfo() { + /* do nothing */ + } + + /** + * Creates a new instance. + * + * @param resolveInfo The service resolve info. + * @param context Context for accessing resources. + * @throws XmlPullParserException If a XML parsing error occurs. + * @throws IOException If a XML parsing error occurs. + * + * @hide + */ + public AccessibilityServiceInfo(ResolveInfo resolveInfo, Context context) + throws XmlPullParserException, IOException { + ServiceInfo serviceInfo = resolveInfo.serviceInfo; + mId = new ComponentName(serviceInfo.packageName, serviceInfo.name).flattenToShortString(); + mResolveInfo = resolveInfo; + + String settingsActivityName = null; + boolean retrieveScreenContent = false; + XmlResourceParser parser = null; + + try { + PackageManager packageManager = context.getPackageManager(); + parser = serviceInfo.loadXmlMetaData(packageManager, + AccessibilityService.SERVICE_META_DATA); + if (parser == null) { + return; + } + + int type = 0; + while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { + type = parser.next(); + } + + String nodeName = parser.getName(); + if (!TAG_ACCESSIBILITY_SERVICE.equals(nodeName)) { + throw new XmlPullParserException( "Meta-data does not start with" + + TAG_ACCESSIBILITY_SERVICE + " tag"); + } + + AttributeSet allAttributes = Xml.asAttributeSet(parser); + Resources resources = packageManager.getResourcesForApplication( + serviceInfo.applicationInfo); + TypedArray asAttributes = resources.obtainAttributes(allAttributes, + com.android.internal.R.styleable.AccessibilityService); + eventTypes = asAttributes.getInt( + com.android.internal.R.styleable.AccessibilityService_accessibilityEventTypes, + 0); + String packageNamez = asAttributes.getString( + com.android.internal.R.styleable.AccessibilityService_packageNames); + if (packageNamez != null) { + packageNames = packageNamez.split("(\\s)*,(\\s)*"); + } + feedbackType = asAttributes.getInt( + com.android.internal.R.styleable.AccessibilityService_accessibilityFeedbackType, + 0); + notificationTimeout = asAttributes.getInt( + com.android.internal.R.styleable.AccessibilityService_notificationTimeout, + 0); + flags = asAttributes.getInt( + com.android.internal.R.styleable.AccessibilityService_accessibilityFlags, 0); + mSettingsActivityName = asAttributes.getString( + com.android.internal.R.styleable.AccessibilityService_settingsActivity); + mCanRetrieveWindowContent = asAttributes.getBoolean( + com.android.internal.R.styleable.AccessibilityService_canRetrieveWindowContent, + false); + asAttributes.recycle(); + } catch (NameNotFoundException e) { + throw new XmlPullParserException( "Unable to create context for: " + + serviceInfo.packageName); + } finally { + if (parser != null) { + parser.close(); + } + } + } + + /** + * Updates the properties that an AccessibilitySerivice can change dynamically. + * + * @param other The info from which to update the properties. + * + * @hide + */ + public void updateDynamicallyConfigurableProperties(AccessibilityServiceInfo other) { + eventTypes = other.eventTypes; + packageNames = other.packageNames; + feedbackType = other.feedbackType; + notificationTimeout = other.notificationTimeout; + flags = other.flags; + } + + /** + * The accessibility service id. + * <p> + * <strong>Generated by the system.</strong> + * </p> + * @return The id. + */ + public String getId() { + return mId; + } + + /** + * The service {@link ResolveInfo}. + * <p> + * <strong>Generated by the system.</strong> + * </p> + * @return The info. + */ + public ResolveInfo getResolveInfo() { + return mResolveInfo; + } + + /** + * The settings activity name. + * <p> + * <strong>Statically set from + * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong> + * </p> + * @return The settings activity name. + */ + public String getSettingsActivityName() { + return mSettingsActivityName; + } + + /** + * Whether this service can retrieve the currently focused window content. + * <p> + * <strong>Statically set from + * {@link AccessibilityService#SERVICE_META_DATA meta-data}.</strong> + * </p> + * @return True screen content is retrieved. + */ + public boolean getCanRetrieveWindowContent() { + return mCanRetrieveWindowContent; + } + + /** + * {@inheritDoc} + */ public int describeContents() { return 0; } @@ -121,6 +320,142 @@ public class AccessibilityServiceInfo implements Parcelable { parcel.writeInt(feedbackType); parcel.writeLong(notificationTimeout); parcel.writeInt(flags); + parcel.writeString(mId); + parcel.writeParcelable(mResolveInfo, 0); + parcel.writeString(mSettingsActivityName); + parcel.writeInt(mCanRetrieveWindowContent ? 1 : 0); + } + + private void initFromParcel(Parcel parcel) { + eventTypes = parcel.readInt(); + packageNames = parcel.readStringArray(); + feedbackType = parcel.readInt(); + notificationTimeout = parcel.readLong(); + flags = parcel.readInt(); + mId = parcel.readString(); + mResolveInfo = parcel.readParcelable(null); + mSettingsActivityName = parcel.readString(); + mCanRetrieveWindowContent = (parcel.readInt() == 1); + } + + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + appendEventTypes(stringBuilder, eventTypes); + stringBuilder.append(", "); + appendPackageNames(stringBuilder, packageNames); + stringBuilder.append(", "); + appendFeedbackTypes(stringBuilder, feedbackType); + stringBuilder.append(", "); + stringBuilder.append("notificationTimeout: ").append(notificationTimeout); + stringBuilder.append(", "); + appendFlags(stringBuilder, flags); + stringBuilder.append(", "); + stringBuilder.append("id: ").append(mId); + stringBuilder.append(", "); + stringBuilder.append("resolveInfo: ").append(mResolveInfo); + stringBuilder.append(", "); + stringBuilder.append("settingsActivityName: ").append(mSettingsActivityName); + stringBuilder.append(", "); + stringBuilder.append("retrieveScreenContent: ").append(mCanRetrieveWindowContent); + return stringBuilder.toString(); + } + + private static void appendFeedbackTypes(StringBuilder stringBuilder, int feedbackTypes) { + stringBuilder.append("feedbackTypes:"); + stringBuilder.append("["); + while (feedbackTypes != 0) { + final int feedbackTypeBit = (1 << Integer.numberOfTrailingZeros(feedbackTypes)); + stringBuilder.append(feedbackTypeToString(feedbackTypeBit)); + feedbackTypes &= ~feedbackTypeBit; + if (feedbackTypes != 0) { + stringBuilder.append(", "); + } + } + stringBuilder.append("]"); + } + + private static void appendPackageNames(StringBuilder stringBuilder, String[] packageNames) { + stringBuilder.append("packageNames:"); + stringBuilder.append("["); + if (packageNames != null) { + final int packageNameCount = packageNames.length; + for (int i = 0; i < packageNameCount; i++) { + stringBuilder.append(packageNames[i]); + if (i < packageNameCount - 1) { + stringBuilder.append(", "); + } + } + } + stringBuilder.append("]"); + } + + private static void appendEventTypes(StringBuilder stringBuilder, int eventTypes) { + stringBuilder.append("eventTypes:"); + stringBuilder.append("["); + while (eventTypes != 0) { + final int eventTypeBit = (1 << Integer.numberOfTrailingZeros(eventTypes)); + stringBuilder.append(AccessibilityEvent.eventTypeToString(eventTypeBit)); + eventTypes &= ~eventTypeBit; + if (eventTypes != 0) { + stringBuilder.append(", "); + } + } + stringBuilder.append("]"); + } + + private static void appendFlags(StringBuilder stringBuilder, int flags) { + stringBuilder.append("flags:"); + stringBuilder.append("["); + while (flags != 0) { + final int flagBit = (1 << Integer.numberOfTrailingZeros(flags)); + stringBuilder.append(flagToString(flagBit)); + flags &= ~flagBit; + if (flags != 0) { + stringBuilder.append(", "); + } + } + stringBuilder.append("]"); + } + + /** + * Returns the string representation of a feedback type. For example, + * {@link #FEEDBACK_SPOKEN} is represented by the string FEEDBACK_SPOKEN. + * + * @param feedbackType The feedback type. + * @return The string representation. + */ + public static String feedbackTypeToString(int feedbackType) { + switch (feedbackType) { + case FEEDBACK_AUDIBLE: + return "FEEDBACK_AUDIBLE"; + case FEEDBACK_HAPTIC: + return "FEEDBACK_HAPTIC"; + case FEEDBACK_GENERIC: + return "FEEDBACK_GENERIC"; + case FEEDBACK_SPOKEN: + return "FEEDBACK_SPOKEN"; + case FEEDBACK_VISUAL: + return "FEEDBACK_VISUAL"; + default: + return null; + } + } + + /** + * Returns the string representation of a flag. For example, + * {@link #DEFAULT} is represented by the string DEFAULT. + * + * @param flag The flag. + * @return The string representation. + */ + public static String flagToString(int flag) { + switch (flag) { + case DEFAULT: + return "DEFAULT"; + default: + return null; + } } /** @@ -130,11 +465,7 @@ public class AccessibilityServiceInfo implements Parcelable { new Parcelable.Creator<AccessibilityServiceInfo>() { public AccessibilityServiceInfo createFromParcel(Parcel parcel) { AccessibilityServiceInfo info = new AccessibilityServiceInfo(); - info.eventTypes = parcel.readInt(); - info.packageNames = parcel.readStringArray(); - info.feedbackType = parcel.readInt(); - info.notificationTimeout = parcel.readLong(); - info.flags = parcel.readInt(); + info.initFromParcel(parcel); return info; } diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 7157def..19f0bf0 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -17,14 +17,72 @@ package android.accessibilityservice; import android.accessibilityservice.AccessibilityServiceInfo; +import android.view.accessibility.AccessibilityNodeInfo; /** - * Interface AccessibilityManagerService#Service implements, and passes to an - * AccessibilityService so it can dynamically configure how the system handles it. + * Interface given to an AccessibilitySerivce to talk to the AccessibilityManagerService. * * @hide */ -oneway interface IAccessibilityServiceConnection { +interface IAccessibilityServiceConnection { void setServiceInfo(in AccessibilityServiceInfo info); + + /** + * Finds an {@link AccessibilityNodeInfo} by accessibility id. + * <p> + * <strong> + * It is a client responsibility to recycle the received info by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * + * @param accessibilityWindowId A unique window id. + * @param accessibilityViewId A unique View accessibility id. + * @return The node info. + */ + AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int accessibilityWindowId, + int accessibilityViewId); + + /** + * Finds {@link AccessibilityNodeInfo}s by View text. The match is case + * insensitive containment. + * <p> + * <strong> + * It is a client responsibility to recycle the received infos by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * + * @param text The searched text. + * @return A list of node info. + */ + List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewText(String text); + + /** + * Finds an {@link AccessibilityNodeInfo} by View id. + * <p> + * <strong> + * It is a client responsibility to recycle the received info by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * + * @param id The id of the node. + * @return The node info. + */ + AccessibilityNodeInfo findAccessibilityNodeInfoByViewId(int viewId); + + /** + * Performs an accessibility action on an {@link AccessibilityNodeInfo}. + * + * @param accessibilityWindowId The id of the window. + * @param accessibilityViewId The of a view in the . + * @return Whether the action was performed. + */ + boolean performAccessibilityAction(int accessibilityWindowId, int accessibilityViewId, + int action); } 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/FloatEvaluator.java b/core/java/android/animation/FloatEvaluator.java index 9e2054d..9463aa1 100644 --- a/core/java/android/animation/FloatEvaluator.java +++ b/core/java/android/animation/FloatEvaluator.java @@ -19,7 +19,7 @@ package android.animation; /** * This evaluator can be used to perform type interpolation between <code>float</code> values. */ -public class FloatEvaluator implements TypeEvaluator { +public class FloatEvaluator implements TypeEvaluator<Number> { /** * This function returns the result of linearly interpolating the start and end values, with @@ -35,8 +35,8 @@ public class FloatEvaluator implements TypeEvaluator { * @return A linear interpolation between the start and end values, given the * <code>fraction</code> parameter. */ - public Object evaluate(float fraction, Object startValue, Object endValue) { - float startFloat = ((Number) startValue).floatValue(); - return startFloat + fraction * (((Number) endValue).floatValue() - startFloat); + public Float evaluate(float fraction, Number startValue, Number endValue) { + float startFloat = startValue.floatValue(); + return startFloat + fraction * (endValue.floatValue() - startFloat); } }
\ No newline at end of file diff --git a/core/java/android/animation/IntEvaluator.java b/core/java/android/animation/IntEvaluator.java index 7288927..34fb0dc 100644 --- a/core/java/android/animation/IntEvaluator.java +++ b/core/java/android/animation/IntEvaluator.java @@ -19,7 +19,7 @@ package android.animation; /** * This evaluator can be used to perform type interpolation between <code>int</code> values. */ -public class IntEvaluator implements TypeEvaluator { +public class IntEvaluator implements TypeEvaluator<Integer> { /** * This function returns the result of linearly interpolating the start and end values, with @@ -35,8 +35,8 @@ public class IntEvaluator implements TypeEvaluator { * @return A linear interpolation between the start and end values, given the * <code>fraction</code> parameter. */ - public Object evaluate(float fraction, Object startValue, Object endValue) { - int startInt = ((Number) startValue).intValue(); - return (int) (startInt + fraction * (((Number) endValue).intValue() - startInt)); + public Integer evaluate(float fraction, Integer startValue, Integer endValue) { + int startInt = startValue; + return (int)(startInt + fraction * (endValue - startInt)); } }
\ No newline at end of file diff --git a/core/java/android/animation/LayoutTransition.java b/core/java/android/animation/LayoutTransition.java index 22dd3c7..d25de97 100644 --- a/core/java/android/animation/LayoutTransition.java +++ b/core/java/android/animation/LayoutTransition.java @@ -18,6 +18,7 @@ package android.animation; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.DecelerateInterpolator; @@ -48,6 +49,18 @@ import java.util.List; * with setting up the basic properties of the animations used in these four situations, * or with setting up custom animations for any or all of the four.</p> * + * <p>By default, the DISAPPEARING animation begins immediately, as does the CHANGE_APPEARING + * animation. The other animations begin after a delay that is set to the default duration + * of the animations. This behavior facilitates a sequence of animations in transitions as + * follows: when an item is being added to a layout, the other children of that container will + * move first (thus creating space for the new item), then the appearing animation will run to + * animate the item being added. Conversely, when an item is removed from a container, the + * animation to remove it will run first, then the animations of the other children in the + * layout will run (closing the gap created in the layout when the item was removed). If this + * default choreography behavior is not desired, the {@link #setDuration(int, long)} and + * {@link #setStartDelay(int, long)} of any or all of the animations can be changed as + * appropriate.</p> + * * <p>The animations specified for the transition, both the defaults and any custom animations * set on the transition object, are templates only. That is, these animations exist to hold the * basic animation properties, such as the duration, start delay, and properties being animated. @@ -58,8 +71,9 @@ import java.util.List; * moving as a result of the layout event) as well as the values that are changing (such as the * position and size of that object). The actual values that are pushed to each animation * depends on what properties are specified for the animation. For example, the default - * CHANGE_APPEARING animation animates <code>left</code>, <code>top</code>, <code>right</code>, - * and <code>bottom</code>. Values for these properties are updated with the pre- and post-layout + * CHANGE_APPEARING animation animates the <code>left</code>, <code>top</code>, <code>right</code>, + * <code>bottom</code>, <code>scrollX</code>, and <code>scrollY</code> properties. + * Values for these properties are updated with the pre- and post-layout * values when the transition begins. Custom animations will be similarly populated with * the target and values being animated, assuming they use ObjectAnimator objects with * property names that are known on the target object.</p> @@ -198,6 +212,14 @@ public class LayoutTransition { */ private ArrayList<TransitionListener> mListeners; + /** + * Controls whether changing animations automatically animate the parent hierarchy as well. + * This behavior prevents artifacts when wrap_content layouts snap to the end state as the + * transition begins, causing visual glitches and clipping. + * Default value is true. + */ + private boolean mAnimateParentHierarchy = true; + /** * Constructs a LayoutTransition object. By default, the object will listen to layout @@ -211,14 +233,17 @@ public class LayoutTransition { PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top", 0, 1); PropertyValuesHolder pvhRight = PropertyValuesHolder.ofInt("right", 0, 1); PropertyValuesHolder pvhBottom = PropertyValuesHolder.ofInt("bottom", 0, 1); + PropertyValuesHolder pvhScrollX = PropertyValuesHolder.ofInt("scrollX", 0, 1); + PropertyValuesHolder pvhScrollY = PropertyValuesHolder.ofInt("scrollY", 0, 1); defaultChangeIn = ObjectAnimator.ofPropertyValuesHolder(this, - pvhLeft, pvhTop, pvhRight, pvhBottom); + pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScrollX, pvhScrollY); defaultChangeIn.setDuration(DEFAULT_DURATION); defaultChangeIn.setStartDelay(mChangingAppearingDelay); defaultChangeIn.setInterpolator(mChangingAppearingInterpolator); defaultChangeOut = defaultChangeIn.clone(); defaultChangeOut.setStartDelay(mChangingDisappearingDelay); defaultChangeOut.setInterpolator(mChangingDisappearingInterpolator); + defaultFadeIn = ObjectAnimator.ofFloat(this, "alpha", 0f, 1f); defaultFadeIn.setDuration(DEFAULT_DURATION); defaultFadeIn.setStartDelay(mAppearingDelay); @@ -560,122 +585,24 @@ public class LayoutTransition { // only animate the views not being added or removed if (child != newView) { - - - // Make a copy of the appropriate animation - final Animator anim = baseAnimator.clone(); - - // Set the target object for the animation - anim.setTarget(child); - - // A ObjectAnimator (or AnimatorSet of them) can extract start values from - // its target object - anim.setupStartValues(); - - // If there's an animation running on this view already, cancel it - Animator currentAnimation = pendingAnimations.get(child); - if (currentAnimation != null) { - currentAnimation.cancel(); - pendingAnimations.remove(child); + setupChangeAnimation(parent, changeReason, baseAnimator, duration, child); + } + } + if (mAnimateParentHierarchy) { + ViewGroup tempParent = parent; + while (tempParent != null) { + ViewParent parentParent = tempParent.getParent(); + if (parentParent instanceof ViewGroup) { + setupChangeAnimation((ViewGroup)parentParent, changeReason, baseAnimator, + duration, tempParent); + tempParent = (ViewGroup) parentParent; + } else { + tempParent = null; } - // Cache the animation in case we need to cancel it later - pendingAnimations.put(child, anim); - - // For the animations which don't get started, we have to have a means of - // removing them from the cache, lest we leak them and their target objects. - // We run an animator for the default duration+100 (an arbitrary time, but one - // which should far surpass the delay between setting them up here and - // handling layout events which start them. - ValueAnimator pendingAnimRemover = ValueAnimator.ofFloat(0f, 1f). - setDuration(duration+100); - pendingAnimRemover.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - pendingAnimations.remove(child); - } - }); - pendingAnimRemover.start(); - - // Add a listener to track layout changes on this view. If we don't get a callback, - // then there's nothing to animate. - final View.OnLayoutChangeListener listener = new View.OnLayoutChangeListener() { - public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - - // Tell the animation to extract end values from the changed object - anim.setupEndValues(); - - long startDelay; - if (changeReason == APPEARING) { - startDelay = mChangingAppearingDelay + staggerDelay; - staggerDelay += mChangingAppearingStagger; - } else { - startDelay = mChangingDisappearingDelay + staggerDelay; - staggerDelay += mChangingDisappearingStagger; - } - anim.setStartDelay(startDelay); - anim.setDuration(duration); - Animator prevAnimation = currentChangingAnimations.get(child); - if (prevAnimation != null) { - prevAnimation.cancel(); - } - Animator pendingAnimation = pendingAnimations.get(child); - if (pendingAnimation != null) { - pendingAnimations.remove(child); - } - // Cache the animation in case we need to cancel it later - currentChangingAnimations.put(child, anim); - - if (anim instanceof ObjectAnimator) { - ((ObjectAnimator) anim).setCurrentPlayTime(0); - } - anim.start(); - - // this only removes listeners whose views changed - must clear the - // other listeners later - child.removeOnLayoutChangeListener(this); - layoutChangeListenerMap.remove(child); - } - }; - // Remove the animation from the cache when it ends - anim.addListener(new AnimatorListenerAdapter() { - - @Override - public void onAnimationStart(Animator animator) { - if (mListeners != null) { - for (TransitionListener listener : mListeners) { - listener.startTransition(LayoutTransition.this, parent, child, - changeReason == APPEARING ? - CHANGE_APPEARING : CHANGE_DISAPPEARING); - } - } - } - - @Override - public void onAnimationCancel(Animator animator) { - child.removeOnLayoutChangeListener(listener); - layoutChangeListenerMap.remove(child); - } - - @Override - public void onAnimationEnd(Animator animator) { - currentChangingAnimations.remove(child); - if (mListeners != null) { - for (TransitionListener listener : mListeners) { - listener.endTransition(LayoutTransition.this, parent, child, - changeReason == APPEARING ? - CHANGE_APPEARING : CHANGE_DISAPPEARING); - } - } - } - }); - - child.addOnLayoutChangeListener(listener); - // cache the listener for later removal - layoutChangeListenerMap.put(child, listener); } } + // This is the cleanup step. When we get this rendering event, we know that all of // the appropriate animations have been set up and run. Now we can clear out the // layout listeners. @@ -694,6 +621,175 @@ public class LayoutTransition { } /** + * This flag controls whether CHANGE_APPEARING or CHANGE_DISAPPEARING animations will + * cause the same changing animation to be run on the parent hierarchy as well. This allows + * containers of transitioning views to also transition, which may be necessary in situations + * where the containers bounds change between the before/after states and may clip their + * children during the transition animations. For example, layouts with wrap_content will + * adjust their bounds according to the dimensions of their children. + * + * @param animateParentHierarchy A boolean value indicating whether the parents of + * transitioning views should also be animated during the transition. Default value is true. + */ + public void setAnimateParentHierarchy(boolean animateParentHierarchy) { + mAnimateParentHierarchy = animateParentHierarchy; + } + + /** + * Utility function called by runChangingTransition for both the children and the parent + * hierarchy. + */ + private void setupChangeAnimation(final ViewGroup parent, final int changeReason, + Animator baseAnimator, final long duration, final View child) { + // Make a copy of the appropriate animation + final Animator anim = baseAnimator.clone(); + + // Set the target object for the animation + anim.setTarget(child); + + // A ObjectAnimator (or AnimatorSet of them) can extract start values from + // its target object + anim.setupStartValues(); + + // If there's an animation running on this view already, cancel it + Animator currentAnimation = pendingAnimations.get(child); + if (currentAnimation != null) { + currentAnimation.cancel(); + pendingAnimations.remove(child); + } + // Cache the animation in case we need to cancel it later + pendingAnimations.put(child, anim); + + // For the animations which don't get started, we have to have a means of + // removing them from the cache, lest we leak them and their target objects. + // We run an animator for the default duration+100 (an arbitrary time, but one + // which should far surpass the delay between setting them up here and + // handling layout events which start them. + ValueAnimator pendingAnimRemover = ValueAnimator.ofFloat(0f, 1f). + setDuration(duration + 100); + pendingAnimRemover.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + pendingAnimations.remove(child); + } + }); + pendingAnimRemover.start(); + + // Add a listener to track layout changes on this view. If we don't get a callback, + // then there's nothing to animate. + final View.OnLayoutChangeListener listener = new View.OnLayoutChangeListener() { + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + + // Tell the animation to extract end values from the changed object + anim.setupEndValues(); + if (anim instanceof ValueAnimator) { + boolean valuesDiffer = false; + ValueAnimator valueAnim = (ValueAnimator)anim; + PropertyValuesHolder[] oldValues = valueAnim.getValues(); + for (int i = 0; i < oldValues.length; ++i) { + PropertyValuesHolder pvh = oldValues[i]; + KeyframeSet keyframeSet = pvh.mKeyframeSet; + if (keyframeSet.mFirstKeyframe == null || + keyframeSet.mLastKeyframe == null || + !keyframeSet.mFirstKeyframe.getValue().equals( + keyframeSet.mLastKeyframe.getValue())) { + valuesDiffer = true; + } + } + if (!valuesDiffer) { + return; + } + } + + long startDelay; + if (changeReason == APPEARING) { + startDelay = mChangingAppearingDelay + staggerDelay; + staggerDelay += mChangingAppearingStagger; + } else { + startDelay = mChangingDisappearingDelay + staggerDelay; + staggerDelay += mChangingDisappearingStagger; + } + anim.setStartDelay(startDelay); + anim.setDuration(duration); + + Animator prevAnimation = currentChangingAnimations.get(child); + if (prevAnimation != null) { + prevAnimation.cancel(); + } + Animator pendingAnimation = pendingAnimations.get(child); + if (pendingAnimation != null) { + pendingAnimations.remove(child); + } + // Cache the animation in case we need to cancel it later + currentChangingAnimations.put(child, anim); + + parent.requestTransitionStart(LayoutTransition.this); + + // this only removes listeners whose views changed - must clear the + // other listeners later + child.removeOnLayoutChangeListener(this); + layoutChangeListenerMap.remove(child); + } + }; + // Remove the animation from the cache when it ends + anim.addListener(new AnimatorListenerAdapter() { + + @Override + public void onAnimationStart(Animator animator) { + if (mListeners != null) { + for (TransitionListener listener : mListeners) { + listener.startTransition(LayoutTransition.this, parent, child, + changeReason == APPEARING ? + CHANGE_APPEARING : CHANGE_DISAPPEARING); + } + } + } + + @Override + public void onAnimationCancel(Animator animator) { + child.removeOnLayoutChangeListener(listener); + layoutChangeListenerMap.remove(child); + } + + @Override + public void onAnimationEnd(Animator animator) { + currentChangingAnimations.remove(child); + if (mListeners != null) { + for (TransitionListener listener : mListeners) { + listener.endTransition(LayoutTransition.this, parent, child, + changeReason == APPEARING ? + CHANGE_APPEARING : CHANGE_DISAPPEARING); + } + } + } + }); + + child.addOnLayoutChangeListener(listener); + // cache the listener for later removal + layoutChangeListenerMap.put(child, listener); + } + + /** + * Starts the animations set up for a CHANGING transition. We separate the setup of these + * animations from actually starting them, to avoid side-effects that starting the animations + * may have on the properties of the affected objects. After setup, we tell the affected parent + * that this transition should be started. The parent informs its ViewAncestor, which then + * starts the transition after the current layout/measurement phase, just prior to drawing + * the view hierarchy. + * + * @hide + */ + public void startChangingAnimations() { + for (Animator anim : currentChangingAnimations.values()) { + if (anim instanceof ObjectAnimator) { + ((ObjectAnimator) anim).setCurrentPlayTime(0); + } + anim.start(); + } + } + + /** * Returns true if animations are running which animate layout-related properties. This * essentially means that either CHANGE_APPEARING or CHANGE_DISAPPEARING animations * are running, since these animations operate on layout-related properties. diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java index b8a7cb2..31c5f8d 100644 --- a/core/java/android/animation/ObjectAnimator.java +++ b/core/java/android/animation/ObjectAnimator.java @@ -17,6 +17,7 @@ package android.animation; import android.util.Log; +import android.util.Property; import java.lang.reflect.Method; import java.util.ArrayList; @@ -39,6 +40,8 @@ public final class ObjectAnimator extends ValueAnimator { private String mPropertyName; + private Property mProperty; + /** * Sets the name of the property that will be animated. This name is used to derive * a setter function that will be called to set animated values. @@ -63,7 +66,7 @@ public final class ObjectAnimator extends ValueAnimator { * using more than one PropertyValuesHolder objects, then setting the propertyName simply * sets the propertyName in the first of those PropertyValuesHolder objects.</p> * - * @param propertyName The name of the property being animated. + * @param propertyName The name of the property being animated. Should not be null. */ public void setPropertyName(String propertyName) { // mValues could be null if this is being constructed piecemeal. Just record the @@ -81,6 +84,31 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Sets the property that will be animated. Property objects will take precedence over + * properties specified by the {@link #setPropertyName(String)} method. Animations should + * be set up to use one or the other, not both. + * + * @param property The property being animated. Should not be null. + */ + public void setProperty(Property property) { + // mValues could be null if this is being constructed piecemeal. Just record the + // propertyName to be used later when setValues() is called if so. + if (mValues != null) { + PropertyValuesHolder valuesHolder = mValues[0]; + String oldName = valuesHolder.getPropertyName(); + valuesHolder.setProperty(property); + mValuesMap.remove(oldName); + mValuesMap.put(mPropertyName, valuesHolder); + } + if (mProperty != null) { + mPropertyName = property.getName(); + } + mProperty = property; + // New property/values/target should cause re-initialization prior to starting + mInitialized = false; + } + + /** * Gets the name of the property that will be animated. This name will be used to derive * a setter function that will be called to set animated values. * For example, a property name of <code>foo</code> will result @@ -93,36 +121,6 @@ public final class ObjectAnimator extends ValueAnimator { } /** - * Determine the setter or getter function using the JavaBeans convention of setFoo or - * getFoo for a property named 'foo'. This function figures out what the name of the - * function should be and uses reflection to find the Method with that name on the - * target object. - * - * @param prefix "set" or "get", depending on whether we need a setter or getter. - * @return Method the method associated with mPropertyName. - */ - private Method getPropertyFunction(String prefix, Class valueType) { - // TODO: faster implementation... - Method returnVal = null; - String firstLetter = mPropertyName.substring(0, 1); - String theRest = mPropertyName.substring(1); - firstLetter = firstLetter.toUpperCase(); - String setterName = prefix + firstLetter + theRest; - Class args[] = null; - if (valueType != null) { - args = new Class[1]; - args[0] = valueType; - } - try { - returnVal = mTarget.getClass().getMethod(setterName, args); - } catch (NoSuchMethodException e) { - Log.e("ObjectAnimator", - "Couldn't find setter/getter for property " + mPropertyName + ": " + e); - } - return returnVal; - } - - /** * Creates a new ObjectAnimator object. This default constructor is primarily for * use internally; the other constructors which take parameters are more generally * useful. @@ -131,8 +129,8 @@ public final class ObjectAnimator extends ValueAnimator { } /** - * A constructor that takes a single property name and set of values. This constructor is - * used in the simple case of animating a single property. + * Private utility constructor that initializes the target object and name of the + * property being animated. * * @param target The object whose property is to be animated. This object should * have a public method on it called <code>setName()</code>, where <code>name</code> is @@ -145,19 +143,29 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Private utility constructor that initializes the target object and property being animated. + * + * @param target The object whose property is to be animated. + * @param property The property being animated. + */ + private <T> ObjectAnimator(T target, Property<T, ?> property) { + mTarget = target; + setProperty(property); + } + + /** * Constructs and returns an ObjectAnimator that animates between int values. A single - * value implies that that value is the one being animated to. However, this is not typically - * useful in a ValueAnimator object because there is no way for the object to determine the - * starting value for the animation (unlike ObjectAnimator, which can derive that value - * from the target object and property being animated). Therefore, there should typically - * be two or more values. + * value implies that that value is the one being animated to. Two values imply a starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). * * @param target The object whose property is to be animated. This object should * have a public method on it called <code>setName()</code>, where <code>name</code> is * the value of the <code>propertyName</code> parameter. * @param propertyName The name of the property being animated. * @param values A set of values that the animation will animate between over time. - * @return A ValueAnimator object that is set up to animate between the given values. + * @return An ObjectAnimator object that is set up to animate between the given values. */ public static ObjectAnimator ofInt(Object target, String propertyName, int... values) { ObjectAnimator anim = new ObjectAnimator(target, propertyName); @@ -166,19 +174,36 @@ public final class ObjectAnimator extends ValueAnimator { } /** + * Constructs and returns an ObjectAnimator that animates between int values. A single + * value implies that that value is the one being animated to. Two values imply a starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). + * + * @param target The object whose property is to be animated. + * @param property The property being animated. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T> ObjectAnimator ofInt(T target, Property<T, Integer> property, int... values) { + ObjectAnimator anim = new ObjectAnimator(target, property); + anim.setIntValues(values); + return anim; + } + + /** * Constructs and returns an ObjectAnimator that animates between float values. A single - * value implies that that value is the one being animated to. However, this is not typically - * useful in a ValueAnimator object because there is no way for the object to determine the - * starting value for the animation (unlike ObjectAnimator, which can derive that value - * from the target object and property being animated). Therefore, there should typically - * be two or more values. + * value implies that that value is the one being animated to. Two values imply a starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). * * @param target The object whose property is to be animated. This object should * have a public method on it called <code>setName()</code>, where <code>name</code> is * the value of the <code>propertyName</code> parameter. * @param propertyName The name of the property being animated. * @param values A set of values that the animation will animate between over time. - * @return A ValueAnimator object that is set up to animate between the given values. + * @return An ObjectAnimator object that is set up to animate between the given values. */ public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) { ObjectAnimator anim = new ObjectAnimator(target, propertyName); @@ -187,21 +212,40 @@ public final class ObjectAnimator extends ValueAnimator { } /** - * A constructor that takes <code>PropertyValueHolder</code> values. This constructor should - * be used when animating several properties at once with the same ObjectAnimator, since - * PropertyValuesHolder allows you to associate a set of animation values with a property - * name. + * Constructs and returns an ObjectAnimator that animates between float values. A single + * value implies that that value is the one being animated to. Two values imply a starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). + * + * @param target The object whose property is to be animated. + * @param property The property being animated. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T> ObjectAnimator ofFloat(T target, Property<T, Float> property, + float... values) { + ObjectAnimator anim = new ObjectAnimator(target, property); + anim.setFloatValues(values); + return anim; + } + + /** + * Constructs and returns an ObjectAnimator that animates between Object values. A single + * value implies that that value is the one being animated to. Two values imply a starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). * * @param target The object whose property is to be animated. This object should - * have public methods on it called <code>setName()</code>, where <code>name</code> is - * the name of the property passed in as the <code>propertyName</code> parameter for - * each of the PropertyValuesHolder objects. + * have a public method on it called <code>setName()</code>, where <code>name</code> is + * the value of the <code>propertyName</code> parameter. * @param propertyName The name of the property being animated. * @param evaluator A TypeEvaluator that will be called on each animation frame to - * provide the ncessry interpolation between the Object values to derive the animated + * provide the necessary interpolation between the Object values to derive the animated * value. - * @param values The PropertyValuesHolder objects which hold each the property name and values - * to animate that property between. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. */ public static ObjectAnimator ofObject(Object target, String propertyName, TypeEvaluator evaluator, Object... values) { @@ -212,19 +256,44 @@ public final class ObjectAnimator extends ValueAnimator { } /** - * Constructs and returns an ObjectAnimator that animates between the sets of values - * specifed in <code>PropertyValueHolder</code> objects. This variant should - * be used when animating several properties at once with the same ObjectAnimator, since - * PropertyValuesHolder allows you to associate a set of animation values with a property - * name. + * Constructs and returns an ObjectAnimator that animates between Object values. A single + * value implies that that value is the one being animated to. Two values imply a starting + * and ending values. More than two values imply a starting value, values to animate through + * along the way, and an ending value (these values will be distributed evenly across + * the duration of the animation). * - * @param target The object whose property is to be animated. This object should - * have public methods on it called <code>setName()</code>, where <code>name</code> is - * the name of the property passed in as the <code>propertyName</code> parameter for - * each of the PropertyValuesHolder objects. - * @param values A set of PropertyValuesHolder objects whose values will be animated - * between over time. - * @return A ValueAnimator object that is set up to animate between the given values. + * @param target The object whose property is to be animated. + * @param property The property being animated. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values A set of values that the animation will animate between over time. + * @return An ObjectAnimator object that is set up to animate between the given values. + */ + public static <T, V> ObjectAnimator ofObject(T target, Property<T, V> property, + TypeEvaluator<V> evaluator, V... values) { + ObjectAnimator anim = new ObjectAnimator(target, property); + anim.setObjectValues(values); + anim.setEvaluator(evaluator); + return anim; + } + + /** + * Constructs and returns an ObjectAnimator that animates between the sets of values specified + * in <code>PropertyValueHolder</code> objects. This variant should be used when animating + * several properties at once with the same ObjectAnimator, since PropertyValuesHolder allows + * you to associate a set of animation values with a property name. + * + * @param target The object whose property is to be animated. Depending on how the + * PropertyValuesObjects were constructed, the target object should either have the {@link + * android.util.Property} objects used to construct the PropertyValuesHolder objects or (if the + * PropertyValuesHOlder objects were created with property names) the target object should have + * public methods on it called <code>setName()</code>, where <code>name</code> is the name of + * the property passed in as the <code>propertyName</code> parameter for each of the + * PropertyValuesHolder objects. + * @param values A set of PropertyValuesHolder objects whose values will be animated between + * over time. + * @return An ObjectAnimator object that is set up to animate between the given values. */ public static ObjectAnimator ofPropertyValuesHolder(Object target, PropertyValuesHolder... values) { @@ -239,7 +308,11 @@ public final class ObjectAnimator extends ValueAnimator { if (mValues == null || mValues.length == 0) { // No values yet - this animator is being constructed piecemeal. Init the values with // whatever the current propertyName is - setValues(PropertyValuesHolder.ofInt(mPropertyName, values)); + if (mProperty != null) { + setValues(PropertyValuesHolder.ofInt(mProperty, values)); + } else { + setValues(PropertyValuesHolder.ofInt(mPropertyName, values)); + } } else { super.setIntValues(values); } @@ -250,7 +323,11 @@ public final class ObjectAnimator extends ValueAnimator { if (mValues == null || mValues.length == 0) { // No values yet - this animator is being constructed piecemeal. Init the values with // whatever the current propertyName is - setValues(PropertyValuesHolder.ofFloat(mPropertyName, values)); + if (mProperty != null) { + setValues(PropertyValuesHolder.ofFloat(mProperty, values)); + } else { + setValues(PropertyValuesHolder.ofFloat(mPropertyName, values)); + } } else { super.setFloatValues(values); } @@ -261,7 +338,11 @@ public final class ObjectAnimator extends ValueAnimator { if (mValues == null || mValues.length == 0) { // No values yet - this animator is being constructed piecemeal. Init the values with // whatever the current propertyName is - setValues(PropertyValuesHolder.ofObject(mPropertyName, (TypeEvaluator)null, values)); + if (mProperty != null) { + setValues(PropertyValuesHolder.ofObject(mProperty, (TypeEvaluator)null, values)); + } else { + setValues(PropertyValuesHolder.ofObject(mPropertyName, (TypeEvaluator)null, values)); + } } else { super.setObjectValues(values); } diff --git a/core/java/android/animation/PropertyValuesHolder.java b/core/java/android/animation/PropertyValuesHolder.java index 6f91fc0..58f23f7 100644 --- a/core/java/android/animation/PropertyValuesHolder.java +++ b/core/java/android/animation/PropertyValuesHolder.java @@ -16,7 +16,10 @@ package android.animation; +import android.util.FloatProperty; +import android.util.IntProperty; import android.util.Log; +import android.util.Property; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -39,6 +42,11 @@ public class PropertyValuesHolder implements Cloneable { String mPropertyName; /** + * @hide + */ + protected Property mProperty; + + /** * The setter function, if needed. ObjectAnimator hands off this functionality to * PropertyValuesHolder, since it holds all of the per-property information. This * property is automatically @@ -124,6 +132,17 @@ public class PropertyValuesHolder implements Cloneable { } /** + * Internal utility constructor, used by the factory methods to set the property. + * @param property The property for this holder. + */ + private PropertyValuesHolder(Property property) { + mProperty = property; + if (property != null) { + mPropertyName = property.getName(); + } + } + + /** * Constructs and returns a PropertyValuesHolder with a given property name and * set of int values. * @param propertyName The name of the property being animated. @@ -131,8 +150,18 @@ public class PropertyValuesHolder implements Cloneable { * @return PropertyValuesHolder The constructed PropertyValuesHolder object. */ public static PropertyValuesHolder ofInt(String propertyName, int... values) { - PropertyValuesHolder pvh = new IntPropertyValuesHolder(propertyName, values); - return pvh; + return new IntPropertyValuesHolder(propertyName, values); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of int values. + * @param property The property being animated. Should not be null. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static PropertyValuesHolder ofInt(Property<?, Integer> property, int... values) { + return new IntPropertyValuesHolder(property, values); } /** @@ -143,18 +172,28 @@ public class PropertyValuesHolder implements Cloneable { * @return PropertyValuesHolder The constructed PropertyValuesHolder object. */ public static PropertyValuesHolder ofFloat(String propertyName, float... values) { - PropertyValuesHolder pvh = new FloatPropertyValuesHolder(propertyName, values); - return pvh; + return new FloatPropertyValuesHolder(propertyName, values); + } + + /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of float values. + * @param property The property being animated. Should not be null. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static PropertyValuesHolder ofFloat(Property<?, Float> property, float... values) { + return new FloatPropertyValuesHolder(property, values); } /** * Constructs and returns a PropertyValuesHolder with a given property name and * set of Object values. This variant also takes a TypeEvaluator because the system - * cannot interpolate between objects of unknown type. + * cannot automatically interpolate between objects of unknown type. * * @param propertyName The name of the property being animated. * @param evaluator A TypeEvaluator that will be called on each animation frame to - * provide the ncessry interpolation between the Object values to derive the animated + * provide the necessary interpolation between the Object values to derive the animated * value. * @param values The values that the named property will animate between. * @return PropertyValuesHolder The constructed PropertyValuesHolder object. @@ -168,6 +207,26 @@ public class PropertyValuesHolder implements Cloneable { } /** + * Constructs and returns a PropertyValuesHolder with a given property and + * set of Object values. This variant also takes a TypeEvaluator because the system + * cannot automatically interpolate between objects of unknown type. + * + * @param property The property being animated. Should not be null. + * @param evaluator A TypeEvaluator that will be called on each animation frame to + * provide the necessary interpolation between the Object values to derive the animated + * value. + * @param values The values that the property will animate between. + * @return PropertyValuesHolder The constructed PropertyValuesHolder object. + */ + public static <V> PropertyValuesHolder ofObject(Property property, + TypeEvaluator<V> evaluator, V... values) { + PropertyValuesHolder pvh = new PropertyValuesHolder(property); + pvh.setObjectValues(values); + pvh.setEvaluator(evaluator); + return pvh; + } + + /** * Constructs and returns a PropertyValuesHolder object with the specified property name and set * of values. These values can be of any type, but the type should be consistent so that * an appropriate {@link android.animation.TypeEvaluator} can be found that matches @@ -202,6 +261,37 @@ public class PropertyValuesHolder implements Cloneable { } /** + * Constructs and returns a PropertyValuesHolder object with the specified property and set + * of values. These values can be of any type, but the type should be consistent so that + * an appropriate {@link android.animation.TypeEvaluator} can be found that matches + * the common type. + * <p>If there is only one value, it is assumed to be the end value of an animation, + * and an initial value will be derived, if possible, by calling the property's + * {@link android.util.Property#get(Object)} function. + * Also, if any value is null, the value will be filled in when the animation + * starts in the same way. This mechanism of automatically getting null values only works + * if the PropertyValuesHolder object is used in conjunction with + * {@link ObjectAnimator}, since otherwise PropertyValuesHolder has + * no way of determining what the value should be. + * @param property The property associated with this set of values. Should not be null. + * @param values The set of values to animate between. + */ + public static PropertyValuesHolder ofKeyframe(Property property, Keyframe... values) { + KeyframeSet keyframeSet = KeyframeSet.ofKeyframe(values); + if (keyframeSet instanceof IntKeyframeSet) { + return new IntPropertyValuesHolder(property, (IntKeyframeSet) keyframeSet); + } else if (keyframeSet instanceof FloatKeyframeSet) { + return new FloatPropertyValuesHolder(property, (FloatKeyframeSet) keyframeSet); + } + else { + PropertyValuesHolder pvh = new PropertyValuesHolder(property); + pvh.mKeyframeSet = keyframeSet; + pvh.mValueType = ((Keyframe)values[0]).getType(); + return pvh; + } + } + + /** * Set the animated values for this object to this set of ints. * If there is only one value, it is assumed to be the end value of an animation, * and an initial value will be derived, if possible, by calling a getter function @@ -349,7 +439,6 @@ public class PropertyValuesHolder implements Cloneable { // Have to lock property map prior to reading it, to guard against // another thread putting something in there after we've checked it // but before we've added an entry to it - // TODO: can we store the setter/getter per Class instead of per Object? mPropertyMapLock.writeLock().lock(); HashMap<String, Method> propertyMap = propertyMapMap.get(targetClass); if (propertyMap != null) { @@ -395,6 +484,22 @@ public class PropertyValuesHolder implements Cloneable { * @param target The object on which the setter (and possibly getter) exist. */ void setupSetterAndGetter(Object target) { + if (mProperty != null) { + // check to make sure that mProperty is on the class of target + try { + Object testValue = mProperty.get(target); + for (Keyframe kf : mKeyframeSet.mKeyframes) { + if (!kf.hasValue()) { + kf.setValue(mProperty.get(target)); + } + } + return; + } catch (ClassCastException e) { + Log.e("PropertyValuesHolder","No such property (" + mProperty.getName() + + ") on target object " + target + ". Trying reflection instead"); + mProperty = null; + } + } Class targetClass = target.getClass(); if (mSetter == null) { setupSetter(targetClass); @@ -423,6 +528,9 @@ public class PropertyValuesHolder implements Cloneable { * @param kf The keyframe which holds the property name and value. */ private void setupValue(Object target, Keyframe kf) { + if (mProperty != null) { + kf.setValue(mProperty.get(target)); + } try { if (mGetter == null) { Class targetClass = target.getClass(); @@ -465,6 +573,7 @@ public class PropertyValuesHolder implements Cloneable { try { PropertyValuesHolder newPVH = (PropertyValuesHolder) super.clone(); newPVH.mPropertyName = mPropertyName; + newPVH.mProperty = mProperty; newPVH.mKeyframeSet = mKeyframeSet.clone(); newPVH.mEvaluator = mEvaluator; return newPVH; @@ -482,6 +591,9 @@ public class PropertyValuesHolder implements Cloneable { * @param target The target object on which the value is set */ void setAnimatedValue(Object target) { + if (mProperty != null) { + mProperty.set(target, getAnimatedValue()); + } if (mSetter != null) { try { mTmpValueArray[0] = getAnimatedValue(); @@ -558,6 +670,18 @@ public class PropertyValuesHolder implements Cloneable { } /** + * Sets the property that will be animated. + * + * <p>Note that if this PropertyValuesHolder object is used with ObjectAnimator, the property + * must exist on the target object specified in that ObjectAnimator.</p> + * + * @param property The property being animated. + */ + public void setProperty(Property property) { + mProperty = property; + } + + /** * Gets the name of the property that will be animated. This name will be used to derive * a setter function that will be called to set animated values. * For example, a property name of <code>foo</code> will result @@ -597,17 +721,22 @@ public class PropertyValuesHolder implements Cloneable { * specified above. */ static String getMethodName(String prefix, String propertyName) { - char firstLetter = propertyName.charAt(0); + if (propertyName == null || propertyName.length() == 0) { + // shouldn't get here + return prefix; + } + char firstLetter = Character.toUpperCase(propertyName.charAt(0)); String theRest = propertyName.substring(1); - firstLetter = Character.toUpperCase(firstLetter); return prefix + firstLetter + theRest; } static class IntPropertyValuesHolder extends PropertyValuesHolder { + // Cache JNI functions to avoid looking them up twice private static final HashMap<Class, HashMap<String, Integer>> sJNISetterPropertyMap = new HashMap<Class, HashMap<String, Integer>>(); int mJniSetter; + private IntProperty mIntProperty; IntKeyframeSet mIntKeyframeSet; int mIntAnimatedValue; @@ -619,11 +748,29 @@ public class PropertyValuesHolder implements Cloneable { mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; } + public IntPropertyValuesHolder(Property property, IntKeyframeSet keyframeSet) { + super(property); + mValueType = int.class; + mKeyframeSet = keyframeSet; + mIntKeyframeSet = (IntKeyframeSet) mKeyframeSet; + if (property instanceof IntProperty) { + mIntProperty = (IntProperty) mProperty; + } + } + public IntPropertyValuesHolder(String propertyName, int... values) { super(propertyName); setIntValues(values); } + public IntPropertyValuesHolder(Property property, int... values) { + super(property); + setIntValues(values); + if (property instanceof IntProperty) { + mIntProperty = (IntProperty) mProperty; + } + } + @Override public void setIntValues(int... values) { super.setIntValues(values); @@ -656,6 +803,14 @@ public class PropertyValuesHolder implements Cloneable { */ @Override void setAnimatedValue(Object target) { + if (mIntProperty != null) { + mIntProperty.setValue(target, mIntAnimatedValue); + return; + } + if (mProperty != null) { + mProperty.set(target, mIntAnimatedValue); + return; + } if (mJniSetter != 0) { nCallIntMethod(target, mJniSetter, mIntAnimatedValue); return; @@ -674,6 +829,9 @@ public class PropertyValuesHolder implements Cloneable { @Override void setupSetter(Class targetClass) { + if (mProperty != null) { + return; + } // Check new static hashmap<propName, int> for setter method try { mPropertyMapLock.writeLock().lock(); @@ -696,7 +854,8 @@ public class PropertyValuesHolder implements Cloneable { } } } catch (NoSuchMethodError e) { - // System.out.println("Can't find native method using JNI, use reflection" + e); + Log.d("PropertyValuesHolder", + "Can't find native method using JNI, use reflection" + e); } finally { mPropertyMapLock.writeLock().unlock(); } @@ -709,9 +868,11 @@ public class PropertyValuesHolder implements Cloneable { static class FloatPropertyValuesHolder extends PropertyValuesHolder { + // Cache JNI functions to avoid looking them up twice private static final HashMap<Class, HashMap<String, Integer>> sJNISetterPropertyMap = new HashMap<Class, HashMap<String, Integer>>(); int mJniSetter; + private FloatProperty mFloatProperty; FloatKeyframeSet mFloatKeyframeSet; float mFloatAnimatedValue; @@ -723,11 +884,29 @@ public class PropertyValuesHolder implements Cloneable { mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; } + public FloatPropertyValuesHolder(Property property, FloatKeyframeSet keyframeSet) { + super(property); + mValueType = float.class; + mKeyframeSet = keyframeSet; + mFloatKeyframeSet = (FloatKeyframeSet) mKeyframeSet; + if (property instanceof FloatProperty) { + mFloatProperty = (FloatProperty) mProperty; + } + } + public FloatPropertyValuesHolder(String propertyName, float... values) { super(propertyName); setFloatValues(values); } + public FloatPropertyValuesHolder(Property property, float... values) { + super(property); + setFloatValues(values); + if (property instanceof FloatProperty) { + mFloatProperty = (FloatProperty) mProperty; + } + } + @Override public void setFloatValues(float... values) { super.setFloatValues(values); @@ -760,6 +939,14 @@ public class PropertyValuesHolder implements Cloneable { */ @Override void setAnimatedValue(Object target) { + if (mFloatProperty != null) { + mFloatProperty.setValue(target, mFloatAnimatedValue); + return; + } + if (mProperty != null) { + mProperty.set(target, mFloatAnimatedValue); + return; + } if (mJniSetter != 0) { nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue); return; @@ -778,6 +965,9 @@ public class PropertyValuesHolder implements Cloneable { @Override void setupSetter(Class targetClass) { + if (mProperty != null) { + return; + } // Check new static hashmap<propName, int> for setter method try { mPropertyMapLock.writeLock().lock(); @@ -800,7 +990,8 @@ public class PropertyValuesHolder implements Cloneable { } } } catch (NoSuchMethodError e) { - // System.out.println("Can't find native method using JNI, use reflection" + e); + Log.d("PropertyValuesHolder", + "Can't find native method using JNI, use reflection" + e); } finally { mPropertyMapLock.writeLock().unlock(); } diff --git a/core/java/android/animation/TypeEvaluator.java b/core/java/android/animation/TypeEvaluator.java index fa49175..e738da1 100644 --- a/core/java/android/animation/TypeEvaluator.java +++ b/core/java/android/animation/TypeEvaluator.java @@ -24,7 +24,7 @@ package android.animation; * * @see ValueAnimator#setEvaluator(TypeEvaluator) */ -public interface TypeEvaluator { +public interface TypeEvaluator<T> { /** * This function returns the result of linearly interpolating the start and end values, with @@ -39,6 +39,6 @@ public interface TypeEvaluator { * @return A linear interpolation between the start and end values, given the * <code>fraction</code> parameter. */ - public Object evaluate(float fraction, Object startValue, Object endValue); + public T evaluate(float fraction, T startValue, T endValue); }
\ No newline at end of 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 b739e10..0481158 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; @@ -1399,6 +1398,10 @@ public class Activity extends ContextThemeWrapper public void onConfigurationChanged(Configuration newConfig) { mCalled = true; + if (mActionBar != null) { + mActionBar.onConfigurationChanged(newConfig); + } + mFragments.dispatchConfigurationChanged(newConfig); if (mWindow != null) { @@ -2488,6 +2491,7 @@ public class Activity extends ContextThemeWrapper break; case Window.FEATURE_ACTION_BAR: + initActionBar(); mActionBar.dispatchMenuVisibilityChanged(false); break; } @@ -3633,7 +3637,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)) { @@ -4571,7 +4575,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 48b8ca8..a6658cc 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; @@ -302,13 +303,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. * @@ -338,7 +332,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. @@ -493,10 +487,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; @@ -712,7 +814,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! @@ -1367,4 +1469,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 88293e8..85f40c9 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -39,7 +39,6 @@ import android.os.Parcel; import android.os.ServiceManager; import android.os.StrictMode; import android.text.TextUtils; -import android.util.Config; import android.util.Log; import android.util.Singleton; @@ -442,10 +441,10 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } - case GET_TASK_THUMBNAIL_TRANSACTION: { + case GET_TASK_THUMBNAILS_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); int id = data.readInt(); - Bitmap bm = getTaskThumbnail(id); + ActivityManager.TaskThumbnails bm = getTaskThumbnails(id); reply.writeNoException(); if (bm != null) { reply.writeInt(1); @@ -1436,6 +1435,53 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM reply.writeNoException(); return true; } + + case SWITCH_USER_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + int userid = data.readInt(); + boolean result = switchUser(userid); + reply.writeNoException(); + reply.writeInt(result ? 1 : 0); + return true; + } + + case REMOVE_SUB_TASK_TRANSACTION: + { + data.enforceInterface(IActivityManager.descriptor); + int taskId = data.readInt(); + int subTaskIndex = data.readInt(); + boolean result = removeSubTask(taskId, subTaskIndex); + reply.writeNoException(); + reply.writeInt(result ? 1 : 0); + return true; + } + + case REMOVE_TASK_TRANSACTION: + { + data.enforceInterface(IActivityManager.descriptor); + int taskId = data.readInt(); + int fl = data.readInt(); + boolean result = removeTask(taskId, fl); + reply.writeNoException(); + reply.writeInt(result ? 1 : 0); + return true; + } + + case REGISTER_PROCESS_OBSERVER_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IProcessObserver observer = IProcessObserver.Stub.asInterface( + data.readStrongBinder()); + registerProcessObserver(observer); + return true; + } + + case UNREGISTER_PROCESS_OBSERVER_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IProcessObserver observer = IProcessObserver.Stub.asInterface( + data.readStrongBinder()); + unregisterProcessObserver(observer); + return true; + } case GET_PACKAGE_ASK_SCREEN_COMPAT_TRANSACTION: { @@ -1469,11 +1515,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; @@ -1890,16 +1936,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(); @@ -3276,5 +3322,68 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); } + public boolean switchUser(int userid) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(userid); + mRemote.transact(SWITCH_USER_TRANSACTION, data, reply, 0); + reply.readException(); + boolean result = reply.readInt() != 0; + reply.recycle(); + data.recycle(); + return result; + } + + public boolean removeSubTask(int taskId, int subTaskIndex) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(taskId); + data.writeInt(subTaskIndex); + mRemote.transact(REMOVE_SUB_TASK_TRANSACTION, data, reply, 0); + reply.readException(); + boolean result = reply.readInt() != 0; + reply.recycle(); + data.recycle(); + return result; + } + + public boolean removeTask(int taskId, int flags) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeInt(taskId); + data.writeInt(flags); + mRemote.transact(REMOVE_TASK_TRANSACTION, data, reply, 0); + reply.readException(); + boolean result = reply.readInt() != 0; + reply.recycle(); + data.recycle(); + return result; + } + + public void registerProcessObserver(IProcessObserver observer) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(observer != null ? observer.asBinder() : null); + mRemote.transact(REGISTER_PROCESS_OBSERVER_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + public void unregisterProcessObserver(IProcessObserver observer) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(observer != null ? observer.asBinder() : null); + mRemote.transact(UNREGISTER_PROCESS_OBSERVER_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + private IBinder mRemote; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 6c63c2a..1ec7a96 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -45,6 +45,7 @@ import android.graphics.Canvas; import android.net.IConnectivityManager; import android.net.Proxy; import android.net.ProxyProperties; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Debug; import android.os.Handler; @@ -59,7 +60,6 @@ import android.os.ServiceManager; import android.os.StrictMode; import android.os.SystemClock; import android.util.AndroidRuntimeException; -import android.util.Config; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.Log; @@ -70,7 +70,7 @@ import android.view.HardwareRenderer; import android.view.View; import android.view.ViewDebug; import android.view.ViewManager; -import android.view.ViewRoot; +import android.view.ViewAncestor; import android.view.Window; import android.view.WindowManager; import android.view.WindowManagerImpl; @@ -124,12 +124,12 @@ public final class ActivityThread { public static final String TAG = "ActivityThread"; private static final android.graphics.Bitmap.Config THUMBNAIL_FORMAT = Bitmap.Config.RGB_565; private static final boolean DEBUG = false; - static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; + static final boolean localLOGV = false; static final boolean DEBUG_MESSAGES = false; /** @hide */ public static final boolean DEBUG_BROADCAST = false; private static final boolean DEBUG_RESULTS = false; - private static final boolean DEBUG_BACKUP = false; + private static final boolean DEBUG_BACKUP = true; private static final boolean DEBUG_CONFIGURATION = false; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; private static final Pattern PATTERN_SEMICOLON = Pattern.compile(";"); @@ -345,6 +345,7 @@ public final class ActivityThread { static final class ServiceArgsData { IBinder token; + boolean taskRemoved; int startId; int flags; Intent args; @@ -408,6 +409,8 @@ public final class ActivityThread { String pkg; CompatibilityInfo info; } + + native private void dumpGraphicsInfo(FileDescriptor fd); private final class ApplicationThread extends ApplicationThreadNative { private static final String HEAP_COLUMN = "%17s %8s %8s %8s %8s"; @@ -558,10 +561,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; @@ -723,9 +727,14 @@ public final class ActivityThread { Slog.w(TAG, "dumpActivity failed", e); } } - + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (args != null && args.length == 1 && args[0].equals("graphics")) { + pw.flush(); + dumpGraphicsInfo(fd); + return; + } long nativeMax = Debug.getNativeHeapSize() / 1024; long nativeAllocated = Debug.getNativeHeapAllocatedSize() / 1024; long nativeFree = Debug.getNativeHeapFreeSize() / 1024; @@ -747,7 +756,7 @@ public final class ActivityThread { long dalvikFree = runtime.freeMemory() / 1024; long dalvikAllocated = dalvikMax - dalvikFree; long viewInstanceCount = ViewDebug.getViewInstanceCount(); - long viewRootInstanceCount = ViewDebug.getViewRootInstanceCount(); + long viewRootInstanceCount = ViewDebug.getViewAncestorInstanceCount(); long appContextInstanceCount = Debug.countInstancesOfClass(ContextImpl.class); long activityInstanceCount = Debug.countInstancesOfClass(Activity.class); int globalAssetCount = AssetManager.getGlobalAssetCount(); @@ -862,7 +871,7 @@ public final class ActivityThread { pw.println(" "); pw.println(" Objects"); - printRow(pw, TWO_COUNT_COLUMNS, "Views:", viewInstanceCount, "ViewRoots:", + printRow(pw, TWO_COUNT_COLUMNS, "Views:", viewInstanceCount, "ViewAncestors:", viewRootInstanceCount); printRow(pw, TWO_COUNT_COLUMNS, "AppContexts:", appContextInstanceCount, @@ -935,7 +944,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; @@ -1153,8 +1162,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; @@ -2003,24 +2012,27 @@ public final class ActivityThread { BackupAgent agent = null; String classname = data.appInfo.backupAgentName; - if (classname == null) { - if (data.backupMode == IApplicationThread.BACKUP_MODE_INCREMENTAL) { - Slog.e(TAG, "Attempted incremental backup but no defined agent for " - + packageName); - return; + + if (data.backupMode == IApplicationThread.BACKUP_MODE_FULL + || data.backupMode == IApplicationThread.BACKUP_MODE_RESTORE_FULL) { + classname = "android.app.backup.FullBackupAgent"; + if ((data.appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { + // system packages can supply their own full-backup agent + if (data.appInfo.fullBackupAgentName != null) { + classname = data.appInfo.fullBackupAgentName; + } } - classname = "android.app.FullBackupAgent"; } + try { IBinder binder = null; try { + if (DEBUG_BACKUP) Slog.v(TAG, "Initializing agent class " + classname); + java.lang.ClassLoader cl = packageInfo.getClassLoader(); - agent = (BackupAgent) cl.loadClass(data.appInfo.backupAgentName).newInstance(); + agent = (BackupAgent) cl.loadClass(classname).newInstance(); // set up the agent's context - if (DEBUG_BACKUP) Slog.v(TAG, "Initializing BackupAgent " - + data.appInfo.backupAgentName); - ContextImpl context = new ContextImpl(); context.init(packageInfo, null, this); context.setOuterContext(agent); @@ -2033,7 +2045,8 @@ public final class ActivityThread { // If this is during restore, fail silently; otherwise go // ahead and let the user see the crash. Slog.e(TAG, "Agent threw during creation: " + e); - if (data.backupMode != IApplicationThread.BACKUP_MODE_RESTORE) { + if (data.backupMode != IApplicationThread.BACKUP_MODE_RESTORE + && data.backupMode != IApplicationThread.BACKUP_MODE_RESTORE_FULL) { throw e; } // falling through with 'binder' still null @@ -2047,7 +2060,7 @@ public final class ActivityThread { } } catch (Exception e) { throw new RuntimeException("Unable to create BackupAgent " - + data.appInfo.backupAgentName + ": " + e.toString(), e); + + classname + ": " + e.toString(), e); } } @@ -2204,7 +2217,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(); @@ -2734,7 +2753,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); } @@ -3492,8 +3511,7 @@ public final class ActivityThread { } final void handleLowMemory() { - ArrayList<ComponentCallbacks> callbacks - = new ArrayList<ComponentCallbacks>(); + ArrayList<ComponentCallbacks> callbacks; synchronized (mPackages) { callbacks = collectComponentCallbacksLocked(true, null); @@ -3525,6 +3543,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 @@ -3684,12 +3710,16 @@ public final class ActivityThread { Application app = data.info.makeApplication(data.restrictedBackupMode, null); mInitialApplication = app; - List<ProviderInfo> providers = data.providers; - if (providers != null) { - installContentProviders(app, providers); - // For process that contains content providers, we want to - // ensure that the JIT is enabled "at some point". - mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000); + // don't bring up providers in restricted mode; they may depend on the + // app's custom Application class + if (!data.restrictedBackupMode){ + List<ProviderInfo> providers = data.providers; + if (providers != null) { + installContentProviders(app, providers); + // For process that contains content providers, we want to + // ensure that the JIT is enabled "at some point". + mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000); + } } try { @@ -3972,7 +4002,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); @@ -4015,7 +4045,7 @@ public final class ActivityThread { sThreadLocal.set(this); mSystemThread = system; if (!system) { - ViewRoot.addFirstDrawHandler(new Runnable() { + ViewAncestor.addFirstDrawHandler(new Runnable() { public void run() { ensureJitEnabled(); } @@ -4045,7 +4075,7 @@ public final class ActivityThread { } } - ViewRoot.addConfigCallback(new ComponentCallbacks() { + ViewAncestor.addConfigCallback(new ComponentCallbacks() { public void onConfigurationChanged(Configuration newConfig) { synchronized (mPackages) { // We need to apply this change to the resources diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 5926929..4cff12f 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -33,7 +33,6 @@ import android.content.pm.IPackageMoveObserver; import android.content.pm.IPackageStatsObserver; import android.content.pm.InstrumentationInfo; import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.PermissionGroupInfo; @@ -41,6 +40,7 @@ import android.content.pm.PermissionInfo; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; @@ -1130,6 +1130,63 @@ final class ApplicationPackageManager extends PackageManager { return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; } + // Multi-user support + + /** + * @hide + */ + @Override + public UserInfo createUser(String name, int flags) { + try { + return mPM.createUser(name, flags); + } catch (RemoteException e) { + // Should never happen! + } + return null; + } + + /** + * @hide + */ + @Override + public List<UserInfo> getUsers() { + // TODO: + // Dummy code, always returns just the primary user + ArrayList<UserInfo> users = new ArrayList<UserInfo>(); + UserInfo primary = new UserInfo(0, "Root!", + UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY); + users.add(primary); + return users; + } + + /** + * @hide + */ + @Override + public boolean removeUser(int id) { + try { + return mPM.removeUser(id); + } catch (RemoteException e) { + return false; + } + } + + /** + * @hide + */ + @Override + public void updateUserName(int id, String name) { + // TODO: + } + + /** + * @hide + */ + @Override + public void updateUserFlags(int id, int flags) { + // TODO: + } + private final ContextImpl mContext; private final IPackageManager mPM; diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index 850ad2b..dc0f529 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -223,6 +223,7 @@ public abstract class ApplicationThreadNative extends Binder { data.enforceInterface(IApplicationThread.descriptor); IBinder token = data.readStrongBinder(); + boolean taskRemoved = data.readInt() != 0; int startId = data.readInt(); int fl = data.readInt(); Intent args; @@ -231,7 +232,7 @@ public abstract class ApplicationThreadNative extends Binder } else { args = null; } - scheduleServiceArgs(token, startId, fl, args); + scheduleServiceArgs(token, taskRemoved, startId, fl, args); return true; } @@ -710,11 +711,12 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } - public final void scheduleServiceArgs(IBinder token, int startId, + public final void scheduleServiceArgs(IBinder token, boolean taskRemoved, int startId, int flags, Intent args) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeStrongBinder(token); + data.writeInt(taskRemoved ? 1 : 0); data.writeInt(startId); data.writeInt(flags); if (args != null) { diff --git a/core/java/android/app/FullBackupAgent.java b/core/java/android/app/FullBackupAgent.java deleted file mode 100644 index acd20bd..0000000 --- a/core/java/android/app/FullBackupAgent.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.app; - -import android.app.backup.BackupAgent; -import android.app.backup.BackupDataInput; -import android.app.backup.BackupDataOutput; -import android.app.backup.FileBackupHelper; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import java.io.File; -import java.util.ArrayList; -import java.util.LinkedList; - -/** - * Backs up an application's entire /data/data/<package>/... file system. This - * class is used by the desktop full backup mechanism and is not intended for direct - * use by applications. - * - * {@hide} - */ - -public class FullBackupAgent extends BackupAgent { - // !!! TODO: turn off debugging - private static final String TAG = "FullBackupAgent"; - private static final boolean DEBUG = true; - - @Override - public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, - ParcelFileDescriptor newState) { - LinkedList<File> dirsToScan = new LinkedList<File>(); - ArrayList<String> allFiles = new ArrayList<String>(); - - // build the list of files in the app's /data/data tree - dirsToScan.add(getFilesDir()); - if (DEBUG) Log.v(TAG, "Backing up dir tree @ " + getFilesDir().getAbsolutePath() + " :"); - while (dirsToScan.size() > 0) { - File dir = dirsToScan.removeFirst(); - File[] contents = dir.listFiles(); - if (contents != null) { - for (File f : contents) { - if (f.isDirectory()) { - dirsToScan.add(f); - } else if (f.isFile()) { - if (DEBUG) Log.v(TAG, " " + f.getAbsolutePath()); - allFiles.add(f.getAbsolutePath()); - } - } - } - } - - // That's the file set; now back it all up - FileBackupHelper helper = new FileBackupHelper(this, - allFiles.toArray(new String[allFiles.size()])); - helper.performBackup(oldState, data, newState); - } - - @Override - public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) { - } -} diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index b0cbbb5..e2588cf 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; @@ -350,6 +350,16 @@ public interface IActivityManager extends IInterface { public boolean getPackageAskScreenCompat(String packageName) throws RemoteException; public void setPackageAskScreenCompat(String packageName, boolean ask) 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; + + public void registerProcessObserver(IProcessObserver observer) throws RemoteException; + public void unregisterProcessObserver(IProcessObserver observer) throws RemoteException; /* * Private non-Binder interfaces @@ -524,7 +534,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; @@ -572,4 +582,9 @@ public interface IActivityManager extends IInterface { int SET_PACKAGE_SCREEN_COMPAT_MODE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+126; int GET_PACKAGE_ASK_SCREEN_COMPAT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+127; int SET_PACKAGE_ASK_SCREEN_COMPAT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+128; + int SWITCH_USER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+129; + int REMOVE_SUB_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+130; + int REMOVE_TASK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+131; + int REGISTER_PROCESS_OBSERVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+132; + int UNREGISTER_PROCESS_OBSERVER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+133; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 93a8ff3..05a68a8 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -68,6 +68,7 @@ public interface IApplicationThread extends IInterface { static final int BACKUP_MODE_INCREMENTAL = 0; static final int BACKUP_MODE_FULL = 1; static final int BACKUP_MODE_RESTORE = 2; + static final int BACKUP_MODE_RESTORE_FULL = 3; void scheduleCreateBackupAgent(ApplicationInfo app, CompatibilityInfo compatInfo, int backupMode) throws RemoteException; void scheduleDestroyBackupAgent(ApplicationInfo app, CompatibilityInfo compatInfo) @@ -78,8 +79,8 @@ public interface IApplicationThread extends IInterface { Intent intent, boolean rebind) throws RemoteException; void scheduleUnbindService(IBinder token, Intent intent) throws RemoteException; - void scheduleServiceArgs(IBinder token, int startId, int flags, Intent args) - throws RemoteException; + void scheduleServiceArgs(IBinder token, boolean taskRemoved, int startId, + int flags, Intent args) throws RemoteException; void scheduleStopService(IBinder token) throws RemoteException; static final int DEBUG_OFF = 0; static final int DEBUG_ON = 1; diff --git a/core/java/android/app/IBackupAgent.aidl b/core/java/android/app/IBackupAgent.aidl index fed2bc5..8af78fa 100644 --- a/core/java/android/app/IBackupAgent.aidl +++ b/core/java/android/app/IBackupAgent.aidl @@ -51,6 +51,7 @@ oneway interface IBackupAgent { void doBackup(in ParcelFileDescriptor oldState, in ParcelFileDescriptor data, in ParcelFileDescriptor newState, + boolean storeApk, int token, IBackupManager callbackBinder); /** @@ -78,4 +79,23 @@ oneway interface IBackupAgent { */ void doRestore(in ParcelFileDescriptor data, int appVersionCode, in ParcelFileDescriptor newState, int token, IBackupManager callbackBinder); + + /** + * Restore a single "file" to the application. The file was typically obtained from + * a full-backup dataset. The agent reads 'size' bytes of file content + * from the provided file descriptor. + * + * @param data Read-only pipe delivering the file content itself. + * + * @param size Size of the file being restored. + * @param type Type of file system entity, e.g. FullBackup.TYPE_DIRECTORY. + * @param domain Name of the file's semantic domain to which the 'path' argument is a + * relative path. e.g. FullBackup.DATABASE_TREE_TOKEN. + * @param path Relative path of the file within its semantic domain. + * @param mode Access mode of the file system entity, e.g. 0660. + * @param mtime Last modification time of the file system entity. + */ + void doRestoreFile(in ParcelFileDescriptor data, long size, + int type, String domain, String path, long mode, long mtime, + int token, IBackupManager callbackBinder); } diff --git a/core/java/android/app/IProcessObserver.aidl b/core/java/android/app/IProcessObserver.aidl new file mode 100644 index 0000000..2094294 --- /dev/null +++ b/core/java/android/app/IProcessObserver.aidl @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +/** {@hide} */ +oneway interface IProcessObserver { + + void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities); + void onProcessDied(int pid, int uid); + +} diff --git a/core/java/android/app/IThumbnailRetriever.aidl b/core/java/android/app/IThumbnailRetriever.aidl new file mode 100644 index 0000000..410cc20 --- /dev/null +++ b/core/java/android/app/IThumbnailRetriever.aidl @@ -0,0 +1,25 @@ +/* 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 + * {@hide} + */ +interface IThumbnailRetriever { + Bitmap getThumbnail(int index); +} diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 3d4c966..2952e6b 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -33,7 +33,6 @@ import android.os.Process; import android.os.SystemClock; import android.os.ServiceManager; import android.util.AndroidRuntimeException; -import android.util.Config; import android.util.Log; import android.view.IWindowManager; import android.view.KeyCharacterMap; diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 6541c54..4913e78 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -61,8 +61,7 @@ import android.util.Log; public class NotificationManager { private static String TAG = "NotificationManager"; - private static boolean DEBUG = false; - private static boolean localLOGV = DEBUG || android.util.Config.LOGV; + private static boolean localLOGV = false; private static INotificationManager sService; diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 05b9781..c179b35 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -371,6 +371,13 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac public static final int START_REDELIVER_INTENT = 3; /** + * Special constant for reporting that we are done processing + * {@link #onTaskRemoved(Intent)}. + * @hide + */ + public static final int START_TASK_REMOVED_COMPLETE = 1000; + + /** * This flag is set in {@link #onStartCommand} if the Intent is a * re-delivery of a previously delivered intent, because the service * had previously returned {@link #START_REDELIVER_INTENT} but had been @@ -500,6 +507,19 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac } /** + * This is called if the service is currently running and the user has + * removed a task that comes from the service's application. If you have + * set {@link android.content.pm.ServiceInfo#FLAG_STOP_WITH_TASK ServiceInfo.FLAG_STOP_WITH_TASK} + * then you will not receive this callback; instead, the service will simply + * be stopped. + * + * @param rootIntent The original root Intent that was used to launch + * the task that is being removed. + */ + public void onTaskRemoved(Intent rootIntent) { + } + + /** * Stop the service, if it was previously started. This is the same as * calling {@link android.content.Context#stopService} for this particular service. * diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 13a8b78..113c610 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -38,7 +38,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.util.DisplayMetrics; import android.util.Log; -import android.view.ViewRoot; +import android.view.ViewAncestor; import java.io.FileOutputStream; import java.io.IOException; @@ -632,7 +632,7 @@ public class WallpaperManager { public void setWallpaperOffsets(IBinder windowToken, float xOffset, float yOffset) { try { //Log.v(TAG, "Sending new wallpaper offsets from app..."); - ViewRoot.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( + ViewAncestor.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( windowToken, xOffset, yOffset, mWallpaperXStep, mWallpaperYStep); //Log.v(TAG, "...app returning after sending offsets!"); } catch (RemoteException e) { @@ -670,7 +670,7 @@ public class WallpaperManager { int x, int y, int z, Bundle extras) { try { //Log.v(TAG, "Sending new wallpaper offsets from app..."); - ViewRoot.getWindowSession(mContext.getMainLooper()).sendWallpaperCommand( + ViewAncestor.getWindowSession(mContext.getMainLooper()).sendWallpaperCommand( windowToken, action, x, y, z, extras, false); //Log.v(TAG, "...app returning after sending offsets!"); } catch (RemoteException e) { @@ -690,7 +690,7 @@ public class WallpaperManager { */ public void clearWallpaperOffsets(IBinder windowToken) { try { - ViewRoot.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( + ViewAncestor.getWindowSession(mContext.getMainLooper()).setWallpaperPosition( windowToken, -1, -1, -1, -1); } catch (RemoteException e) { // Ignore. diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java index 1c7eb98..1c37414 100644 --- a/core/java/android/app/admin/DeviceAdminInfo.java +++ b/core/java/android/app/admin/DeviceAdminInfo.java @@ -130,6 +130,14 @@ public final class DeviceAdminInfo implements Parcelable { */ public static final int USES_ENCRYPTED_STORAGE = 7; + /** + * A type of policy that this device admin can use: disables use of all device cameras. + * + * <p>To control this policy, the device admin must have a "disable-camera" + * tag in the "uses-policies" section of its meta-data. + */ + public static final int USES_POLICY_DISABLE_CAMERA = 8; + /** @hide */ public static class PolicyInfo { public final int ident; @@ -174,6 +182,9 @@ public final class DeviceAdminInfo implements Parcelable { sPoliciesDisplayOrder.add(new PolicyInfo(USES_ENCRYPTED_STORAGE, "encrypted-storage", com.android.internal.R.string.policylab_encryptedStorage, com.android.internal.R.string.policydesc_encryptedStorage)); + sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_DISABLE_CAMERA, "disable-camera", + com.android.internal.R.string.policylab_disableCamera, + com.android.internal.R.string.policydesc_disableCamera)); for (int i=0; i<sPoliciesDisplayOrder.size(); i++) { PolicyInfo pi = sPoliciesDisplayOrder.get(i); @@ -365,7 +376,8 @@ public final class DeviceAdminInfo implements Parcelable { * {@link #USES_POLICY_LIMIT_PASSWORD}, {@link #USES_POLICY_WATCH_LOGIN}, * {@link #USES_POLICY_RESET_PASSWORD}, {@link #USES_POLICY_FORCE_LOCK}, * {@link #USES_POLICY_WIPE_DATA}, - * {@link #USES_POLICY_EXPIRE_PASSWORD}, {@link #USES_ENCRYPTED_STORAGE}. + * {@link #USES_POLICY_EXPIRE_PASSWORD}, {@link #USES_ENCRYPTED_STORAGE}, + * {@link #USES_POLICY_DISABLE_CAMERA}. */ public boolean usesPolicy(int policyIdent) { return (mUsesPolicies & (1<<policyIdent)) != 0; 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/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index efe2633..4147b0f 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -1228,6 +1228,45 @@ public class DevicePolicyManager { } /** + * Called by an application that is administering the device to disable all cameras + * on the device. After setting this, no applications will be able to access any cameras + * on the device. + * + * <p>The calling device admin must have requested + * {@link DeviceAdminInfo#USES_POLICY_DISABLE_CAMERA} to be able to call + * this method; if it has not, a security exception will be thrown. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param disabled Whether or not the camera should be disabled. + */ + public void setCameraDisabled(ComponentName admin, boolean disabled) { + if (mService != null) { + try { + mService.setCameraDisabled(admin, disabled); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Determine whether or not the device's cameras have been disabled either by the current + * admin, if specified, or all admins. + * @param admin The name of the admin component to check, or null to check if any admins + * have disabled the camera + */ + public boolean getCameraDisabled(ComponentName admin) { + if (mService != null) { + try { + return mService.getCameraDisabled(admin); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return false; + } + + /** * @hide */ public void setActiveAdmin(ComponentName policyReceiver, boolean refreshing) { diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index e8caca1..9419a62 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -79,6 +79,9 @@ interface IDevicePolicyManager { boolean getStorageEncryption(in ComponentName who); int getStorageEncryptionStatus(); + void setCameraDisabled(in ComponentName who, boolean disabled); + boolean getCameraDisabled(in ComponentName who); + void setActiveAdmin(in ComponentName policyReceiver, boolean refreshing); boolean isAdminActive(in ComponentName policyReceiver); List<ComponentName> getActiveAdmins(); diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index cb4e0e7..63f3258 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -85,7 +85,7 @@ import java.io.IOException; */ public abstract class BackupAgent extends ContextWrapper { private static final String TAG = "BackupAgent"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; public BackupAgent() { super(null); @@ -172,11 +172,26 @@ public abstract class BackupAgent extends ContextWrapper { * @param newState An open, read/write ParcelFileDescriptor pointing to an * empty file. The application should record the final backup * state here after restoring its data from the <code>data</code> stream. + * When a full-backup dataset is being restored, this will be <code>null</code>. */ public abstract void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException; + /** + * @hide + */ + public void onRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String path, long mode, long mtime) + throws IOException { + // empty stub implementation + } + + /** + * Package-private, used only for dispatching an extra step during full backup + */ + void onSaveApk(BackupDataOutput data) { + } // ----- Core implementation ----- @@ -196,15 +211,22 @@ public abstract class BackupAgent extends ContextWrapper { private class BackupServiceBinder extends IBackupAgent.Stub { private static final String TAG = "BackupServiceBinder"; + @Override public void doBackup(ParcelFileDescriptor oldState, ParcelFileDescriptor data, ParcelFileDescriptor newState, + boolean storeApk, int token, IBackupManager callbackBinder) throws RemoteException { // Ensure that we're running with the app's normal permission level long ident = Binder.clearCallingIdentity(); if (DEBUG) Log.v(TAG, "doBackup() invoked"); BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor()); + + if (storeApk) { + onSaveApk(output); + } + try { BackupAgent.this.onBackup(oldState, output, newState); } catch (IOException ex) { @@ -223,6 +245,7 @@ public abstract class BackupAgent extends ContextWrapper { } } + @Override public void doRestore(ParcelFileDescriptor data, int appVersionCode, ParcelFileDescriptor newState, int token, IBackupManager callbackBinder) throws RemoteException { @@ -248,5 +271,24 @@ public abstract class BackupAgent extends ContextWrapper { } } } + + @Override + public void doRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String path, long mode, long mtime, + int token, IBackupManager callbackBinder) throws RemoteException { + long ident = Binder.clearCallingIdentity(); + try { + BackupAgent.this.onRestoreFile(data, size, type, domain, path, mode, mtime); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + Binder.restoreCallingIdentity(ident); + try { + callbackBinder.opComplete(token); + } catch (RemoteException e) { + // we'll time out anyway, so we're safe + } + } + } } } diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java new file mode 100644 index 0000000..3b70e19 --- /dev/null +++ b/core/java/android/app/backup/FullBackup.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import libcore.io.ErrnoException; +import libcore.io.Libcore; + +/** + * Global constant definitions et cetera related to the full-backup-to-fd + * binary format. + * + * @hide + */ +public class FullBackup { + static final String TAG = "FullBackup"; + + public static final String APK_TREE_TOKEN = "a"; + public static final String OBB_TREE_TOKEN = "obb"; + public static final String ROOT_TREE_TOKEN = "r"; + public static final String DATA_TREE_TOKEN = "f"; + public static final String DATABASE_TREE_TOKEN = "db"; + public static final String SHAREDPREFS_TREE_TOKEN = "sp"; + public static final String CACHE_TREE_TOKEN = "c"; + public static final String SHARED_STORAGE_TOKEN = "shared"; + + public static final String APPS_PREFIX = "apps/"; + public static final String SHARED_PREFIX = SHARED_STORAGE_TOKEN + "/"; + + public static final String FULL_BACKUP_INTENT_ACTION = "fullback"; + public static final String FULL_RESTORE_INTENT_ACTION = "fullrest"; + public static final String CONF_TOKEN_INTENT_EXTRA = "conftoken"; + + public static final int TYPE_EOF = 0; + public static final int TYPE_FILE = 1; + public static final int TYPE_DIRECTORY = 2; + public static final int TYPE_SYMLINK = 3; + + static public native int backupToTar(String packageName, String domain, + String linkdomain, String rootpath, String path, BackupDataOutput output); + + static public void restoreToFile(ParcelFileDescriptor data, + long size, int type, long mode, long mtime, File outFile, + boolean doChmod) throws IOException { + if (type == FullBackup.TYPE_DIRECTORY) { + // Canonically a directory has no associated content, so we don't need to read + // anything from the pipe in this case. Just create the directory here and + // drop down to the final metadata adjustment. + if (outFile != null) outFile.mkdirs(); + } else { + FileOutputStream out = null; + + // Pull the data from the pipe, copying it to the output file, until we're done + try { + if (outFile != null) { + File parent = outFile.getParentFile(); + if (!parent.exists()) { + // in practice this will only be for the default semantic directories, + // and using the default mode for those is appropriate. + // TODO: support the edge case of apps that have adjusted the + // permissions on these core directories + parent.mkdirs(); + } + out = new FileOutputStream(outFile); + } + } catch (IOException e) { + Log.e(TAG, "Unable to create/open file " + outFile.getPath(), e); + } + + byte[] buffer = new byte[32 * 1024]; + final long origSize = size; + FileInputStream in = new FileInputStream(data.getFileDescriptor()); + while (size > 0) { + int toRead = (size > buffer.length) ? buffer.length : (int)size; + int got = in.read(buffer, 0, toRead); + if (got <= 0) { + Log.w(TAG, "Incomplete read: expected " + size + " but got " + + (origSize - size)); + break; + } + if (out != null) { + try { + out.write(buffer, 0, got); + } catch (IOException e) { + // Problem writing to the file. Quit copying data and delete + // the file, but of course keep consuming the input stream. + Log.e(TAG, "Unable to write to file " + outFile.getPath(), e); + out.close(); + out = null; + outFile.delete(); + } + } + size -= got; + } + if (out != null) out.close(); + } + + // Now twiddle the state to match the backup, assuming all went well + if (doChmod && outFile != null) { + try { + Libcore.os.chmod(outFile.getPath(), (int)mode); + } catch (ErrnoException e) { + e.rethrowAsIOException(); + } + outFile.setLastModified(mtime); + } + } +} diff --git a/core/java/android/app/backup/FullBackupAgent.java b/core/java/android/app/backup/FullBackupAgent.java new file mode 100644 index 0000000..df1c363 --- /dev/null +++ b/core/java/android/app/backup/FullBackupAgent.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import libcore.io.Libcore; +import libcore.io.ErrnoException; +import libcore.io.OsConstants; +import libcore.io.StructStat; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.LinkedList; + +/** + * Backs up an application's entire /data/data/<package>/... file system. This + * class is used by the desktop full backup mechanism and is not intended for direct + * use by applications. + * + * {@hide} + */ + +public class FullBackupAgent extends BackupAgent { + // !!! TODO: turn off debugging + private static final String TAG = "FullBackupAgent"; + private static final boolean DEBUG = true; + + PackageManager mPm; + + private String mMainDir; + private String mFilesDir; + private String mDatabaseDir; + private String mSharedPrefsDir; + private String mCacheDir; + private String mLibDir; + + private File NULL_FILE; + + @Override + public void onCreate() { + NULL_FILE = new File("/dev/null"); + + mPm = getPackageManager(); + try { + ApplicationInfo appInfo = mPm.getApplicationInfo(getPackageName(), 0); + mMainDir = new File(appInfo.dataDir).getAbsolutePath(); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Unable to find package " + getPackageName()); + throw new RuntimeException(e); + } + + mFilesDir = getFilesDir().getAbsolutePath(); + mDatabaseDir = getDatabasePath("foo").getParentFile().getAbsolutePath(); + mSharedPrefsDir = getSharedPrefsFile("foo").getParentFile().getAbsolutePath(); + mCacheDir = getCacheDir().getAbsolutePath(); + + ApplicationInfo app = getApplicationInfo(); + mLibDir = (app.nativeLibraryDir != null) + ? new File(app.nativeLibraryDir).getAbsolutePath() + : null; + } + + @Override + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) throws IOException { + // Filters, the scan queue, and the set of resulting entities + HashSet<String> filterSet = new HashSet<String>(); + String packageName = getPackageName(); + + // Okay, start with the app's root tree, but exclude all of the canonical subdirs + if (mLibDir != null) { + filterSet.add(mLibDir); + } + filterSet.add(mCacheDir); + filterSet.add(mDatabaseDir); + filterSet.add(mSharedPrefsDir); + filterSet.add(mFilesDir); + processTree(packageName, FullBackup.ROOT_TREE_TOKEN, mMainDir, filterSet, data); + + // Now do the same for the files dir, db dir, and shared prefs dir + filterSet.add(mMainDir); + filterSet.remove(mFilesDir); + processTree(packageName, FullBackup.DATA_TREE_TOKEN, mFilesDir, filterSet, data); + + filterSet.add(mFilesDir); + filterSet.remove(mDatabaseDir); + processTree(packageName, FullBackup.DATABASE_TREE_TOKEN, mDatabaseDir, filterSet, data); + + filterSet.add(mDatabaseDir); + filterSet.remove(mSharedPrefsDir); + processTree(packageName, FullBackup.SHAREDPREFS_TREE_TOKEN, mSharedPrefsDir, filterSet, data); + } + + // Scan the dir tree (if it actually exists) and process each entry we find. If the + // 'excludes' parameter is non-null, it is consulted each time a new file system entity + // is visited to see whether that entity (and its subtree, if appropriate) should be + // omitted from the backup process. + protected void processTree(String packageName, String domain, String rootPath, + HashSet<String> excludes, BackupDataOutput data) { + File rootFile = new File(rootPath); + if (rootFile.exists()) { + LinkedList<File> scanQueue = new LinkedList<File>(); + scanQueue.add(rootFile); + + while (scanQueue.size() > 0) { + File file = scanQueue.remove(0); + String filePath = file.getAbsolutePath(); + + // prune this subtree? + if (excludes != null && excludes.contains(filePath)) { + continue; + } + + // If it's a directory, enqueue its contents for scanning. + try { + StructStat stat = Libcore.os.lstat(filePath); + if (OsConstants.S_ISLNK(stat.st_mode)) { + if (DEBUG) Log.i(TAG, "Symlink (skipping)!: " + file); + continue; + } else if (OsConstants.S_ISDIR(stat.st_mode)) { + File[] contents = file.listFiles(); + if (contents != null) { + for (File entry : contents) { + scanQueue.add(0, entry); + } + } + } + } catch (ErrnoException e) { + if (DEBUG) Log.w(TAG, "Error scanning file " + file + " : " + e); + continue; + } + + // Finally, back this file up before proceeding + FullBackup.backupToTar(packageName, domain, null, rootPath, filePath, data); + } + } + } + + @Override + void onSaveApk(BackupDataOutput data) { + ApplicationInfo app = getApplicationInfo(); + if (DEBUG) Log.i(TAG, "APK flags: system=" + ((app.flags & ApplicationInfo.FLAG_SYSTEM) != 0) + + " updated=" + ((app.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) + + " locked=" + ((app.flags & ApplicationInfo.FLAG_FORWARD_LOCK) != 0) ); + if (DEBUG) Log.i(TAG, "codepath: " + getPackageCodePath()); + + // Forward-locked apps, system-bundled .apks, etc are filtered out before we get here + final String pkgName = getPackageName(); + final String apkDir = new File(getPackageCodePath()).getParent(); + FullBackup.backupToTar(pkgName, FullBackup.APK_TREE_TOKEN, null, + apkDir, getPackageCodePath(), data); + + // Save associated .obb content if it exists and we did save the apk + // check for .obb and save those too + final File obbDir = Environment.getExternalStorageAppObbDirectory(pkgName); + if (obbDir != null) { + if (DEBUG) Log.i(TAG, "obb dir: " + obbDir.getAbsolutePath()); + File[] obbFiles = obbDir.listFiles(); + if (obbFiles != null) { + final String obbDirName = obbDir.getAbsolutePath(); + for (File obb : obbFiles) { + FullBackup.backupToTar(pkgName, FullBackup.OBB_TREE_TOKEN, null, + obbDirName, obb.getAbsolutePath(), data); + } + } + } + } + + /** + * Dummy -- We're never used for restore of an incremental dataset + */ + @Override + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) + throws IOException { + } + + /** + * Restore the described file from the given pipe. + */ + @Override + public void onRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String relpath, long mode, long mtime) + throws IOException { + String basePath = null; + File outFile = null; + + if (DEBUG) Log.d(TAG, "onRestoreFile() size=" + size + " type=" + type + + " domain=" + domain + " relpath=" + relpath + " mode=" + mode + + " mtime=" + mtime); + + // Parse out the semantic domains into the correct physical location + if (domain.equals(FullBackup.DATA_TREE_TOKEN)) basePath = mFilesDir; + else if (domain.equals(FullBackup.DATABASE_TREE_TOKEN)) basePath = mDatabaseDir; + else if (domain.equals(FullBackup.ROOT_TREE_TOKEN)) basePath = mMainDir; + else if (domain.equals(FullBackup.SHAREDPREFS_TREE_TOKEN)) basePath = mSharedPrefsDir; + + // Not a supported output location? We need to consume the data + // anyway, so send it to /dev/null + outFile = (basePath != null) ? new File(basePath, relpath) : null; + if (DEBUG) Log.i(TAG, "[" + domain + " : " + relpath + "] mapped to " + outFile.getPath()); + + // Now that we've figured out where the data goes, send it on its way + FullBackup.restoreToFile(data, size, type, mode, mtime, outFile, true); + } +} diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index b315b3a..bac874e 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -16,7 +16,9 @@ package android.app.backup; +import android.app.backup.IFullBackupRestoreObserver; import android.app.backup.IRestoreSession; +import android.os.ParcelFileDescriptor; import android.content.Intent; /** @@ -121,6 +123,50 @@ interface IBackupManager { void backupNow(); /** + * Write a full backup of the given package to the supplied file descriptor. + * The fd may be a socket or other non-seekable destination. If no package names + * are supplied, then every application on the device will be backed up to the output. + * + * <p>This method is <i>synchronous</i> -- it does not return until the backup has + * completed. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + * + * @param fd The file descriptor to which a 'tar' file stream is to be written + * @param includeApks If <code>true</code>, the resulting tar stream will include the + * application .apk files themselves as well as their data. + * @param includeShared If <code>true</code>, the resulting tar stream will include + * the contents of the device's shared storage (SD card or equivalent). + * @param allApps If <code>true</code>, the resulting tar stream will include all + * installed applications' data, not just those named in the <code>packageNames</code> + * parameter. + * @param packageNames The package names of the apps whose data (and optionally .apk files) + * are to be backed up. The <code>allApps</code> parameter supersedes this. + */ + void fullBackup(in ParcelFileDescriptor fd, boolean includeApks, boolean includeShared, + boolean allApps, in String[] packageNames); + + /** + * Restore device content from the data stream passed through the given socket. The + * data stream must be in the format emitted by fullBackup(). + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + */ + void fullRestore(in ParcelFileDescriptor fd); + + /** + * Confirm that the requested full backup/restore operation can proceed. The system will + * not actually perform the operation described to fullBackup() / fullRestore() unless the + * UI calls back into the Backup Manager to confirm, passing the correct token. At + * the same time, the UI supplies a callback Binder for progress notifications during + * the operation. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + */ + void acknowledgeFullBackupOrRestore(int token, boolean allow, + IFullBackupRestoreObserver observer); + + /** * Identify the currently selected transport. Callers must hold the * android.permission.BACKUP permission to use this method. */ diff --git a/core/java/android/app/backup/IFullBackupRestoreObserver.aidl b/core/java/android/app/backup/IFullBackupRestoreObserver.aidl new file mode 100644 index 0000000..3e0b73d --- /dev/null +++ b/core/java/android/app/backup/IFullBackupRestoreObserver.aidl @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +/** + * Observer of a full backup or restore process. The observer is told "interesting" + * information about an ongoing full backup or restore action. + * + * {@hide} + */ + +oneway interface IFullBackupRestoreObserver { + /** + * Notification: a full backup operation has begun. + */ + void onStartBackup(); + + /** + * Notification: the system has begun backing up the given package. + * + * @param name The name of the application being saved. This will typically be a + * user-meaningful name such as "Browser" rather than a package name such as + * "com.android.browser", though this is not guaranteed. + */ + void onBackupPackage(String name); + + /** + * Notification: the full backup operation has ended. + */ + void onEndBackup(); + + /** + * Notification: a restore-from-full-backup operation has begun. + */ + void onStartRestore(); + + /** + * Notification: the system has begun restore of the given package. + * + * @param name The name of the application being saved. This will typically be a + * user-meaningful name such as "Browser" rather than a package name such as + * "com.android.browser", though this is not guaranteed. + */ + void onRestorePackage(String name); + + /** + * Notification: the restore-from-full-backup operation has ended. + */ + void onEndRestore(); + + /** + * The user's window of opportunity for confirming the operation has timed out. + */ + void onTimeout(); +} diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 88e00b3..a8c31f9 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -699,7 +699,7 @@ public final class BluetoothAdapter { public boolean cancelDiscovery() { if (getState() != STATE_ON) return false; try { - mService.cancelDiscovery(); + return mService.cancelDiscovery(); } catch (RemoteException e) {Log.e(TAG, "", e);} return false; } 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/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java index 719d730..9a13c3e 100644 --- a/core/java/android/bluetooth/BluetoothSocket.java +++ b/core/java/android/bluetooth/BluetoothSocket.java @@ -94,10 +94,16 @@ public final class BluetoothSocket implements Closeable { private int mPort; /* RFCOMM channel or L2CAP psm */ + private enum SocketState { + INIT, + CONNECTED, + CLOSED + } + /** prevents all native calls after destroyNative() */ - private boolean mClosed; + private SocketState mSocketState; - /** protects mClosed */ + /** protects mSocketState */ private final ReentrantReadWriteLock mLock; /** used by native code only */ @@ -145,7 +151,7 @@ public final class BluetoothSocket implements Closeable { } mInputStream = new BluetoothInputStream(this); mOutputStream = new BluetoothOutputStream(this); - mClosed = false; + mSocketState = SocketState.INIT; mLock = new ReentrantReadWriteLock(); } @@ -195,13 +201,14 @@ public final class BluetoothSocket implements Closeable { public void connect() throws IOException { mLock.readLock().lock(); try { - if (mClosed) throw new IOException("socket closed"); + if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed"); if (mSdp != null) { mPort = mSdp.doSdp(); // blocks } connectNative(); // blocks + mSocketState = SocketState.CONNECTED; } finally { mLock.readLock().unlock(); } @@ -216,7 +223,7 @@ public final class BluetoothSocket implements Closeable { // abort blocking operations on the socket mLock.readLock().lock(); try { - if (mClosed) return; + if (mSocketState == SocketState.CLOSED) return; if (mSdp != null) { mSdp.cancel(); } @@ -229,7 +236,7 @@ public final class BluetoothSocket implements Closeable { // abortNative(), so this lock should immediately acquire mLock.writeLock().lock(); try { - mClosed = true; + mSocketState = SocketState.CLOSED; destroyNative(); } finally { mLock.writeLock().unlock(); @@ -267,13 +274,23 @@ public final class BluetoothSocket implements Closeable { } /** + * Get the connection status of this socket, ie, whether there is an active connection with + * remote device. + * @return true if connected + * false if not connected + */ + public boolean isConnected() { + return (mSocketState == SocketState.CONNECTED); + } + + /** * Currently returns unix errno instead of throwing IOException, * so that BluetoothAdapter can check the error code for EADDRINUSE */ /*package*/ int bindListen() { mLock.readLock().lock(); try { - if (mClosed) return EBADFD; + if (mSocketState == SocketState.CLOSED) return EBADFD; return bindListenNative(); } finally { mLock.readLock().unlock(); @@ -283,8 +300,11 @@ public final class BluetoothSocket implements Closeable { /*package*/ BluetoothSocket accept(int timeout) throws IOException { mLock.readLock().lock(); try { - if (mClosed) throw new IOException("socket closed"); - return acceptNative(timeout); + if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed"); + + BluetoothSocket acceptedSocket = acceptNative(timeout); + mSocketState = SocketState.CONNECTED; + return acceptedSocket; } finally { mLock.readLock().unlock(); } @@ -293,7 +313,7 @@ public final class BluetoothSocket implements Closeable { /*package*/ int available() throws IOException { mLock.readLock().lock(); try { - if (mClosed) throw new IOException("socket closed"); + if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed"); return availableNative(); } finally { mLock.readLock().unlock(); @@ -303,7 +323,7 @@ public final class BluetoothSocket implements Closeable { /*package*/ int read(byte[] b, int offset, int length) throws IOException { mLock.readLock().lock(); try { - if (mClosed) throw new IOException("socket closed"); + if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed"); return readNative(b, offset, length); } finally { mLock.readLock().unlock(); @@ -313,7 +333,7 @@ public final class BluetoothSocket implements Closeable { /*package*/ int write(byte[] b, int offset, int length) throws IOException { mLock.readLock().lock(); try { - if (mClosed) throw new IOException("socket closed"); + if (mSocketState == SocketState.CLOSED) throw new IOException("socket closed"); return writeNative(b, offset, length); } finally { mLock.readLock().unlock(); 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/Context.java b/core/java/android/content/Context.java index 4c7d87f..aecec66 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -1544,6 +1544,11 @@ public abstract class Context { */ public static final String NETWORKMANAGEMENT_SERVICE = "network_management"; + /** {@hide} */ + public static final String NETWORK_STATS_SERVICE = "netstats"; + /** {@hide} */ + public static final String NETWORK_POLICY_SERVICE = "netpolicy"; + /** * Use with {@link #getSystemService} to retrieve a {@link * android.net.wifi.WifiManager} for handling management of diff --git a/core/java/android/content/IOnPrimaryClipChangedListener.aidl b/core/java/android/content/IOnPrimaryClipChangedListener.aidl index fb42a45..46d7f7c 100644 --- a/core/java/android/content/IOnPrimaryClipChangedListener.aidl +++ b/core/java/android/content/IOnPrimaryClipChangedListener.aidl @@ -16,6 +16,9 @@ package android.content; +/** + * {@hide} + */ oneway interface IOnPrimaryClipChangedListener { void dispatchPrimaryClipChanged(); } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 7bdd1b9..2f9627a 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1160,6 +1160,15 @@ public class Intent implements Parcelable, Cloneable { public static final String ACTION_UPGRADE_SETUP = "android.intent.action.UPGRADE_SETUP"; /** + * Activity Action: Show settings for managing network data usage of a + * specific application. Applications should define an activity that offers + * options to control data usage. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_MANAGE_NETWORK_USAGE = + "android.intent.action.MANAGE_NETWORK_USAGE"; + + /** * A string associated with a {@link #ACTION_UPGRADE_SETUP} activity * describing the last run version of the platform that was setup. * @hide @@ -1654,8 +1663,9 @@ public class Intent implements Parcelable, Cloneable { * This is used mainly for the USB Settings panel. * Apps should listen for ACTION_MEDIA_MOUNTED and ACTION_MEDIA_UNMOUNTED broadcasts to be notified * when the SD card file system is mounted or unmounted + * @deprecated replaced by android.os.storage.StorageEventListener */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + @Deprecated public static final String ACTION_UMS_CONNECTED = "android.intent.action.UMS_CONNECTED"; /** @@ -1663,8 +1673,9 @@ public class Intent implements Parcelable, Cloneable { * This is used mainly for the USB Settings panel. * Apps should listen for ACTION_MEDIA_MOUNTED and ACTION_MEDIA_UNMOUNTED broadcasts to be notified * when the SD card file system is mounted or unmounted + * @deprecated replaced by android.os.storage.StorageEventListener */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + @Deprecated public static final String ACTION_UMS_DISCONNECTED = "android.intent.action.UMS_DISCONNECTED"; /** @@ -1878,7 +1889,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 +1919,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 5ba5fe1..f3b1d94 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -21,7 +21,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; @@ -669,7 +668,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()) { @@ -1094,14 +1093,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); @@ -1116,7 +1115,7 @@ public class IntentFilter implements Parcelable { String categoryMismatch = matchCategories(categories); if (categoryMismatch != null) { - if (Config.LOGV) { + if (false) { Log.v(logTag, "No matching category " + categoryMismatch + " for " + this); } return NO_MATCH_CATEGORY; diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 2bd632d..c0a1d8e 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -89,7 +89,16 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * <p>If android:allowBackup is set to false, this attribute is ignored. */ public String backupAgentName; - + + /** + * Class implementing the package's *full* backup functionality. This + * is not usable except by system-installed packages. It can be the same + * as the backupAgent. + * + * @hide + */ + public String fullBackupAgentName; + /** * Value for {@link #flags}: if set, this application is installed in the * device's system image. @@ -538,6 +547,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeInt(installLocation); dest.writeString(manageSpaceActivityName); dest.writeString(backupAgentName); + dest.writeString(fullBackupAgentName); dest.writeInt(descriptionRes); } @@ -574,6 +584,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { installLocation = source.readInt(); manageSpaceActivityName = source.readString(); backupAgentName = source.readString(); + fullBackupAgentName = source.readString(); descriptionRes = source.readInt(); } diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 20b1b50..37b6822 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -36,6 +36,7 @@ import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.UserInfo; import android.net.Uri; import android.content.IntentSender; @@ -342,4 +343,7 @@ interface IPackageManager { boolean setInstallLocation(int loc); int getInstallLocation(); + + UserInfo createUser(in String name, int flags); + boolean removeUser(int userId); } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index bb4a5ce..33c2937 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 @@ -964,11 +969,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 @@ -979,7 +984,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; @@ -992,7 +997,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. @@ -1000,7 +1005,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 @@ -1008,12 +1013,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. @@ -1109,16 +1114,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). @@ -1140,7 +1145,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. * @@ -1163,7 +1168,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. * @@ -1186,12 +1191,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 */ @@ -1238,7 +1243,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. @@ -1253,7 +1258,7 @@ public abstract class PackageManager { * @see #GET_SERVICES * @see #GET_SIGNATURES * @see #GET_UNINSTALLED_PACKAGES - * + * */ public abstract List<PackageInfo> getInstalledPackages(int flags); @@ -1315,7 +1320,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 @@ -1402,7 +1407,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 @@ -1423,38 +1428,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(!!). */ @@ -1463,7 +1468,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. */ @@ -1480,7 +1485,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 @@ -1779,7 +1784,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. * @@ -1800,7 +1805,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. * @@ -1833,7 +1838,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. * @@ -1967,7 +1972,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); @@ -1984,7 +1989,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 @@ -2044,11 +2049,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 @@ -2103,7 +2108,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); @@ -2128,7 +2133,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); @@ -2204,7 +2209,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 @@ -2227,7 +2232,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 @@ -2336,7 +2341,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 @@ -2377,4 +2382,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 98ce8aa..31ad6e9 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -24,11 +24,11 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.os.Binder; import android.os.Build; import android.os.Bundle; import android.os.PatternMatcher; import android.util.AttributeSet; -import android.util.Config; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; @@ -384,7 +384,7 @@ public class PackageParser { return null; } - if ((flags&PARSE_CHATTY) != 0 && Config.LOGD) Log.d( + if ((flags&PARSE_CHATTY) != 0 && false) Log.d( TAG, "Scanning package: " + mArchiveSourcePath); XmlResourceParser parser = null; @@ -666,7 +666,7 @@ public class PackageParser { outError[0] = "No start tag found"; return null; } - if ((flags&PARSE_CHATTY) != 0 && Config.LOGV) Log.v( + if ((flags&PARSE_CHATTY) != 0 && false) Log.v( TAG, "Root element name: '" + parser.getName() + "'"); if (!parser.getName().equals("manifest")) { outError[0] = "No <manifest> tag"; @@ -701,7 +701,7 @@ public class PackageParser { outError[0] = "No start tag found"; return null; } - if ((flags&PARSE_CHATTY) != 0 && Config.LOGV) Log.v( + if ((flags&PARSE_CHATTY) != 0 && false) Log.v( TAG, "Root element name: '" + parser.getName() + "'"); if (!parser.getName().equals("manifest")) { outError[0] = "No <manifest> tag"; @@ -1516,7 +1516,18 @@ public class PackageParser { } } } - + + // fullBackupAgent is explicitly handled even if allowBackup is false + name = sa.getNonConfigurationString( + com.android.internal.R.styleable.AndroidManifestApplication_fullBackupAgent, 0); + if (name != null) { + ai.fullBackupAgentName = buildClassName(pkgName, name, outError); + if (false) { + Log.v(TAG, "android:fullBackupAgent=" + ai.fullBackupAgentName + + " from " + pkgName + "+" + name); + } + } + TypedValue v = sa.peekValue( com.android.internal.R.styleable.AndroidManifestApplication_label); if (v != null && (ai.labelRes=v.resourceId) == 0) { @@ -2480,6 +2491,13 @@ public class PackageParser { s.info.permission = str.length() > 0 ? str.toString().intern() : null; } + s.info.flags = 0; + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestService_stopWithTask, + false)) { + s.info.flags |= ServiceInfo.FLAG_STOP_WITH_TASK; + } + sa.recycle(); if ((owner.applicationInfo.flags&ApplicationInfo.FLAG_CANT_SAVE_STATE) != 0) { diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 087a4fe..612e345 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -33,17 +33,35 @@ public class ServiceInfo extends ComponentInfo */ public String permission; + /** + * Bit in {@link #flags}: If set, the service will automatically be + * stopped by the system if the user removes a task that is rooted + * in one of the application's activities. Set from the + * {@link android.R.attr#stopWithTask} attribute. + */ + public static final int FLAG_STOP_WITH_TASK = 0x0001; + + /** + * Options that have been set in the service declaration in the + * manifest. + * These include: + * {@link #FLAG_STOP_WITH_TASK} + */ + public int flags; + public ServiceInfo() { } public ServiceInfo(ServiceInfo orig) { super(orig); permission = orig.permission; + flags = orig.flags; } public void dump(Printer pw, String prefix) { super.dumpFront(pw, prefix); pw.println(prefix + "permission=" + permission); + pw.println(prefix + "flags=0x" + Integer.toHexString(flags)); } public String toString() { @@ -59,6 +77,7 @@ public class ServiceInfo extends ComponentInfo public void writeToParcel(Parcel dest, int parcelableFlags) { super.writeToParcel(dest, parcelableFlags); dest.writeString(permission); + dest.writeInt(flags); } public static final Creator<ServiceInfo> CREATOR = @@ -74,5 +93,6 @@ public class ServiceInfo extends ComponentInfo private ServiceInfo(Parcel source) { super(source); permission = source.readString(); + flags = source.readInt(); } } diff --git a/core/java/android/content/pm/UserInfo.aidl b/core/java/android/content/pm/UserInfo.aidl new file mode 100644 index 0000000..2e7cb8f --- /dev/null +++ b/core/java/android/content/pm/UserInfo.aidl @@ -0,0 +1,20 @@ +/* +** +** Copyright 2011, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.content.pm; + +parcelable UserInfo; diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java new file mode 100644 index 0000000..ba5331c --- /dev/null +++ b/core/java/android/content/pm/UserInfo.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Per-user information. + * @hide + */ +public class UserInfo implements Parcelable { + /** + * Primary user. Only one user can have this flag set. Meaning of this + * flag TBD. + */ + public static final int FLAG_PRIMARY = 0x00000001; + + /** + * User with administrative privileges. Such a user can create and + * delete users. + */ + public static final int FLAG_ADMIN = 0x00000002; + + /** + * Indicates a guest user that may be transient. + */ + public static final int FLAG_GUEST = 0x00000004; + + public int id; + public String name; + public int flags; + + public UserInfo(int id, String name, int flags) { + this.id = id; + this.name = name; + this.flags = flags; + } + + public boolean isPrimary() { + return (flags & FLAG_PRIMARY) == FLAG_PRIMARY; + } + + public boolean isAdmin() { + return (flags & FLAG_ADMIN) == FLAG_ADMIN; + } + + public boolean isGuest() { + return (flags & FLAG_GUEST) == FLAG_GUEST; + } + + public UserInfo() { + } + + public UserInfo(UserInfo orig) { + name = orig.name; + id = orig.id; + flags = orig.flags; + } + + @Override + public String toString() { + return "UserInfo{" + id + ":" + name + ":" + Integer.toHexString(flags) + "}"; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int parcelableFlags) { + dest.writeInt(id); + dest.writeString(name); + dest.writeInt(flags); + } + + public static final Parcelable.Creator<UserInfo> CREATOR + = new Parcelable.Creator<UserInfo>() { + public UserInfo createFromParcel(Parcel source) { + return new UserInfo(source); + } + public UserInfo[] newArray(int size) { + return new UserInfo[size]; + } + }; + + private UserInfo(Parcel source) { + id = source.readInt(); + name = source.readString(); + flags = source.readInt(); + } +} diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index dbb4271..931cb18 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -17,7 +17,6 @@ package android.content.res; import android.os.ParcelFileDescriptor; -import android.util.Config; import android.util.Log; import android.util.TypedValue; @@ -58,7 +57,7 @@ public final class AssetManager { public static final int ACCESS_BUFFER = 3; private static final String TAG = "AssetManager"; - private static final boolean localLOGV = Config.LOGV || false; + private static final boolean localLOGV = false || false; private static final boolean DEBUG_REFS = false; diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index e2c6483..906a564 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -277,6 +277,26 @@ public final class Configuration implements Parcelable, Comparable<Configuration public int compatSmallestScreenWidthDp; /** + * @hide + */ + public static final int LAYOUT_DIRECTION_UNDEFINED = -1; + + /** + * @hide + */ + public static final int LAYOUT_DIRECTION_LTR = 0; + + /** + * @hide + */ + public static final int LAYOUT_DIRECTION_RTL = 1; + + /** + * @hide The layout direction associated to the current Locale + */ + public int layoutDirection; + + /** * @hide Internal book-keeping. */ public int seq; @@ -302,6 +322,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration mnc = o.mnc; if (o.locale != null) { locale = (Locale) o.locale.clone(); + layoutDirection = o.layoutDirection; } userSetLocale = o.userSetLocale; touchscreen = o.touchscreen; @@ -429,6 +450,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration case NAVIGATIONHIDDEN_YES: sb.append("/h"); break; default: sb.append("/"); sb.append(navigationHidden); break; } + switch (layoutDirection) { + case LAYOUT_DIRECTION_UNDEFINED: sb.append(" ?layoutdir"); break; + case LAYOUT_DIRECTION_LTR: sb.append(" ltr"); break; + case LAYOUT_DIRECTION_RTL: sb.append(" rtl"); break; + } if (seq != 0) { sb.append(" s."); sb.append(seq); @@ -458,6 +484,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration screenHeightDp = compatScreenHeightDp = SCREEN_HEIGHT_DP_UNDEFINED; smallestScreenWidthDp = compatSmallestScreenWidthDp = SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; seq = 0; + layoutDirection = LAYOUT_DIRECTION_LTR; } /** {@hide} */ @@ -492,6 +519,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration changed |= ActivityInfo.CONFIG_LOCALE; locale = delta.locale != null ? (Locale) delta.locale.clone() : null; + layoutDirection = getLayoutDirectionFromLocale(locale); } if (delta.userSetLocale && (!userSetLocale || ((changed & ActivityInfo.CONFIG_LOCALE) != 0))) { @@ -581,6 +609,29 @@ public final class Configuration implements Parcelable, Comparable<Configuration } /** + * Return the layout direction for a given Locale + * @param locale the Locale for which we want the layout direction. Can be null. + * @return the layout direction. This may be one of {@link #LAYOUT_DIRECTION_UNDEFINED}, + * {@link #LAYOUT_DIRECTION_LTR} or {@link #LAYOUT_DIRECTION_RTL}. + * + * @hide + */ + public static int getLayoutDirectionFromLocale(Locale locale) { + if (locale == null || locale.equals(Locale.ROOT)) return LAYOUT_DIRECTION_UNDEFINED; + // Be careful: this code will need to be changed when vertical scripts will be supported + // OR if ICU4C is updated to have the "likelySubtags" file + switch(Character.getDirectionality(locale.getDisplayName(locale).charAt(0))) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + return LAYOUT_DIRECTION_LTR; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + return LAYOUT_DIRECTION_RTL; + default: + return LAYOUT_DIRECTION_UNDEFINED; + } + } + + /** * Return a bit mask of the differences between this Configuration * object and the given one. Does not change the values of either. Any * undefined fields in <var>delta</var> are ignored. @@ -759,6 +810,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration dest.writeInt(compatScreenWidthDp); dest.writeInt(compatScreenHeightDp); dest.writeInt(compatSmallestScreenWidthDp); + dest.writeInt(layoutDirection); dest.writeInt(seq); } @@ -786,6 +838,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration compatScreenWidthDp = source.readInt(); compatScreenHeightDp = source.readInt(); compatSmallestScreenWidthDp = source.readInt(); + layoutDirection = source.readInt(); seq = source.readInt(); } 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/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index b6aca2b..8f8eb6e 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -24,8 +24,8 @@ import android.util.Log; import java.util.Iterator; import java.util.Map; -import java.util.Set; import java.util.Map.Entry; +import java.util.Set; import java.util.regex.Pattern; /** @@ -43,7 +43,7 @@ public class SQLiteQueryBuilder private StringBuilder mWhereClause = null; // lazily created private boolean mDistinct; private SQLiteDatabase.CursorFactory mFactory; - private boolean mStrictProjectionMap; + private boolean mStrict; public SQLiteQueryBuilder() { mDistinct = false; @@ -145,10 +145,28 @@ public class SQLiteQueryBuilder } /** - * @hide + * When set, the selection is verified against malicious arguments. + * When using this class to create a statement using + * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)}, + * non-numeric limits will raise an exception. If a projection map is specified, fields + * not in that map will be ignored. + * If this class is used to execute the statement directly using + * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)} + * or + * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)}, + * additionally also parenthesis escaping selection are caught. + * + * To summarize: To get maximum protection against malicious third party apps (for example + * content provider consumers), make sure to do the following: + * <ul> + * <li>Set this value to true</li> + * <li>Use a projection map</li> + * <li>Use one of the query overloads instead of getting the statement as a sql string</li> + * </ul> + * By default, this value is false. */ - public void setStrictProjectionMap(boolean flag) { - mStrictProjectionMap = flag; + public void setStrict(boolean flag) { + mStrict = flag; } /** @@ -217,13 +235,6 @@ public class SQLiteQueryBuilder } } - private static void appendClauseEscapeClause(StringBuilder s, String name, String clause) { - if (!TextUtils.isEmpty(clause)) { - s.append(name); - DatabaseUtils.appendEscapedSQLString(s, clause); - } - } - /** * Add the names that are non-null in columns to s, separating * them with commas. @@ -320,6 +331,19 @@ public class SQLiteQueryBuilder return null; } + if (mStrict && selection != null && selection.length() > 0) { + // Validate the user-supplied selection to detect syntactic anomalies + // in the selection string that could indicate a SQL injection attempt. + // The idea is to ensure that the selection clause is a valid SQL expression + // by compiling it twice: once wrapped in parentheses and once as + // originally specified. An attacker cannot create an expression that + // would escape the SQL expression while maintaining balanced parentheses + // in both the wrapped and original forms. + String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy, + having, sortOrder, limit); + validateSql(db, sqlForValidation); // will throw if query is invalid + } + String sql = buildQuery( projectionIn, selection, groupBy, having, sortOrder, limit); @@ -329,7 +353,20 @@ public class SQLiteQueryBuilder } return db.rawQueryWithFactory( mFactory, sql, selectionArgs, - SQLiteDatabase.findEditTable(mTables)); + SQLiteDatabase.findEditTable(mTables)); // will throw if query is invalid + } + + /** + * Verifies that a SQL statement is valid by compiling it. + * If the SQL statement is not valid, this method will throw a {@link SQLiteException}. + */ + private void validateSql(SQLiteDatabase db, String sql) { + db.lock(sql); + try { + new SQLiteCompiledSql(db, sql).releaseSqlStatement(); + } finally { + db.unlock(); + } } /** @@ -541,7 +578,7 @@ public class SQLiteQueryBuilder continue; } - if (!mStrictProjectionMap && + if (!mStrict && ( userColumn.contains(" AS ") || userColumn.contains(" as "))) { /* A column alias already exist */ projection[i] = userColumn; diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index c76cc6c..ff973a7 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -80,7 +80,8 @@ public class SQLiteStatement extends SQLiteProgram */ public int executeUpdateDelete() { try { - long timeStart = acquireAndLock(WRITE); + saveSqlAsLastSqlStatement(); + acquireAndLock(WRITE); int numChanges = 0; if ((mStatementType & STATEMENT_DONT_PREPARE) > 0) { // since the statement doesn't have to be prepared, @@ -90,7 +91,6 @@ public class SQLiteStatement extends SQLiteProgram } else { numChanges = native_execute(); } - mDatabase.logTimeStat(mSql, timeStart); return numChanges; } finally { releaseAndUnlock(); @@ -108,15 +108,22 @@ public class SQLiteStatement extends SQLiteProgram */ public long executeInsert() { try { - long timeStart = acquireAndLock(WRITE); - long lastInsertedRowId = native_executeInsert(); - mDatabase.logTimeStat(mSql, timeStart); - return lastInsertedRowId; + saveSqlAsLastSqlStatement(); + acquireAndLock(WRITE); + return native_executeInsert(); } finally { releaseAndUnlock(); } } + private void saveSqlAsLastSqlStatement() { + if (((mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == + DatabaseUtils.STATEMENT_UPDATE) || + (mStatementType & SQLiteProgram.STATEMENT_TYPE_MASK) == + DatabaseUtils.STATEMENT_BEGIN) { + mDatabase.setLastSqlStatement(mSql); + } + } /** * Execute a statement that returns a 1 by 1 table with a numeric value. * For example, SELECT COUNT(*) FROM table; @@ -199,7 +206,7 @@ public class SQLiteStatement extends SQLiteProgram * <li>if the SQL statement is an update, start transaction if not already in one. * otherwise, get lock on the database</li> * <li>acquire reference on this object</li> - * <li>and then return the current time _before_ the database lock was acquired</li> + * <li>and then return the current time _after_ the database lock was acquired</li> * </ul> * <p> * This method removes the duplicate code from the other public @@ -243,7 +250,7 @@ public class SQLiteStatement extends SQLiteProgram } // do I have database lock? if not, grab it. if (!mDatabase.isDbLockedByCurrentThread()) { - mDatabase.lock(); + mDatabase.lock(mSql); mState = LOCK_ACQUIRED; } diff --git a/core/java/android/ddm/DdmHandleAppName.java b/core/java/android/ddm/DdmHandleAppName.java index 4a57d12..78dd23e 100644 --- a/core/java/android/ddm/DdmHandleAppName.java +++ b/core/java/android/ddm/DdmHandleAppName.java @@ -19,7 +19,6 @@ package android.ddm; import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; -import android.util.Config; import android.util.Log; import java.nio.ByteBuffer; @@ -88,7 +87,7 @@ public class DdmHandleAppName extends ChunkHandler { * Send an APNM (APplication NaMe) chunk. */ private static void sendAPNM(String appName) { - if (Config.LOGV) + if (false) Log.v("ddm", "Sending app name"); ByteBuffer out = ByteBuffer.allocate(4 + appName.length()*2); diff --git a/core/java/android/ddm/DdmHandleExit.java b/core/java/android/ddm/DdmHandleExit.java index 8a0b9a4..74ae37a 100644 --- a/core/java/android/ddm/DdmHandleExit.java +++ b/core/java/android/ddm/DdmHandleExit.java @@ -19,7 +19,6 @@ package android.ddm; import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; -import android.util.Config; import android.util.Log; import java.nio.ByteBuffer; @@ -59,7 +58,7 @@ public class DdmHandleExit extends ChunkHandler { * Handle a chunk of data. We're only registered for "EXIT". */ public Chunk handleChunk(Chunk request) { - if (Config.LOGV) + if (false) Log.v("ddm-exit", "Handling " + name(request.type) + " chunk"); /* diff --git a/core/java/android/ddm/DdmHandleHeap.java b/core/java/android/ddm/DdmHandleHeap.java index fa0fbbf..cece556 100644 --- a/core/java/android/ddm/DdmHandleHeap.java +++ b/core/java/android/ddm/DdmHandleHeap.java @@ -21,7 +21,6 @@ import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import org.apache.harmony.dalvik.ddmc.DdmVmInternal; import android.os.Debug; -import android.util.Config; import android.util.Log; import java.io.IOException; import java.nio.ByteBuffer; @@ -78,7 +77,7 @@ public class DdmHandleHeap extends ChunkHandler { * Handle a chunk of data. */ public Chunk handleChunk(Chunk request) { - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Handling " + name(request.type) + " chunk"); int type = request.type; @@ -113,7 +112,7 @@ public class DdmHandleHeap extends ChunkHandler { ByteBuffer in = wrapChunk(request); int when = in.get(); - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Heap segment enable: when=" + when); boolean ok = DdmVmInternal.heapInfoNotify(when); @@ -132,7 +131,7 @@ public class DdmHandleHeap extends ChunkHandler { int when = in.get(); int what = in.get(); - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Heap segment enable: when=" + when + ", what=" + what + ", isNative=" + isNative); @@ -160,7 +159,7 @@ public class DdmHandleHeap extends ChunkHandler { /* get the filename for the output file */ int len = in.getInt(); String fileName = getString(in, len); - if (Config.LOGD) + if (false) Log.d("ddm-heap", "Heap dump: file='" + fileName + "'"); try { @@ -192,7 +191,7 @@ public class DdmHandleHeap extends ChunkHandler { byte result; /* get the filename for the output file */ - if (Config.LOGD) + if (false) Log.d("ddm-heap", "Heap dump: [DDMS]"); String failMsg = null; @@ -218,7 +217,7 @@ public class DdmHandleHeap extends ChunkHandler { private Chunk handleHPGC(Chunk request) { //ByteBuffer in = wrapChunk(request); - if (Config.LOGD) + if (false) Log.d("ddm-heap", "Heap GC request"); System.gc(); @@ -234,7 +233,7 @@ public class DdmHandleHeap extends ChunkHandler { enable = (in.get() != 0); - if (Config.LOGD) + if (false) Log.d("ddm-heap", "Recent allocation enable request: " + enable); DdmVmInternal.enableRecentAllocations(enable); @@ -259,7 +258,7 @@ public class DdmHandleHeap extends ChunkHandler { private Chunk handleREAL(Chunk request) { //ByteBuffer in = wrapChunk(request); - if (Config.LOGD) + if (false) Log.d("ddm-heap", "Recent allocations request"); /* generate the reply in a ready-to-go format */ diff --git a/core/java/android/ddm/DdmHandleHello.java b/core/java/android/ddm/DdmHandleHello.java index 714a611..5088d22 100644 --- a/core/java/android/ddm/DdmHandleHello.java +++ b/core/java/android/ddm/DdmHandleHello.java @@ -19,7 +19,6 @@ package android.ddm; import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; -import android.util.Config; import android.util.Log; import android.os.Debug; @@ -53,7 +52,7 @@ public class DdmHandleHello extends ChunkHandler { * send messages to the server. */ public void connected() { - if (Config.LOGV) + if (false) Log.v("ddm-hello", "Connected!"); if (false) { @@ -70,7 +69,7 @@ public class DdmHandleHello extends ChunkHandler { * periodic transmissions or clean up saved state. */ public void disconnected() { - if (Config.LOGV) + if (false) Log.v("ddm-hello", "Disconnected!"); } @@ -78,7 +77,7 @@ public class DdmHandleHello extends ChunkHandler { * Handle a chunk of data. */ public Chunk handleChunk(Chunk request) { - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Handling " + name(request.type) + " chunk"); int type = request.type; @@ -105,7 +104,7 @@ public class DdmHandleHello extends ChunkHandler { ByteBuffer in = wrapChunk(request); int serverProtoVers = in.getInt(); - if (Config.LOGV) + if (false) Log.v("ddm-hello", "Server version is " + serverProtoVers); /* @@ -150,7 +149,7 @@ public class DdmHandleHello extends ChunkHandler { // is actually compiled in final String[] features = Debug.getVmFeatureList(); - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Got feature list request"); int size = 4 + 4 * features.length; diff --git a/core/java/android/ddm/DdmHandleProfiling.java b/core/java/android/ddm/DdmHandleProfiling.java index 63ee445..e0db5e7 100644 --- a/core/java/android/ddm/DdmHandleProfiling.java +++ b/core/java/android/ddm/DdmHandleProfiling.java @@ -20,7 +20,6 @@ import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import android.os.Debug; -import android.util.Config; import android.util.Log; import java.io.IOException; import java.nio.ByteBuffer; @@ -69,7 +68,7 @@ public class DdmHandleProfiling extends ChunkHandler { * Handle a chunk of data. */ public Chunk handleChunk(Chunk request) { - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Handling " + name(request.type) + " chunk"); int type = request.type; @@ -99,7 +98,7 @@ public class DdmHandleProfiling extends ChunkHandler { int flags = in.getInt(); int len = in.getInt(); String fileName = getString(in, len); - if (Config.LOGV) + if (false) Log.v("ddm-heap", "Method profiling start: filename='" + fileName + "', size=" + bufferSize + ", flags=" + flags); @@ -139,7 +138,7 @@ public class DdmHandleProfiling extends ChunkHandler { int bufferSize = in.getInt(); int flags = in.getInt(); - if (Config.LOGV) { + if (false) { Log.v("ddm-heap", "Method prof stream start: size=" + bufferSize + ", flags=" + flags); } @@ -158,7 +157,7 @@ public class DdmHandleProfiling extends ChunkHandler { private Chunk handleMPSE(Chunk request) { byte result; - if (Config.LOGV) { + if (false) { Log.v("ddm-heap", "Method prof stream end"); } diff --git a/core/java/android/ddm/DdmHandleThread.java b/core/java/android/ddm/DdmHandleThread.java index c307988..613ab75 100644 --- a/core/java/android/ddm/DdmHandleThread.java +++ b/core/java/android/ddm/DdmHandleThread.java @@ -20,7 +20,6 @@ import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; import org.apache.harmony.dalvik.ddmc.DdmVmInternal; -import android.util.Config; import android.util.Log; import java.nio.ByteBuffer; @@ -66,7 +65,7 @@ public class DdmHandleThread extends ChunkHandler { * Handle a chunk of data. */ public Chunk handleChunk(Chunk request) { - if (Config.LOGV) + if (false) Log.v("ddm-thread", "Handling " + name(request.type) + " chunk"); int type = request.type; diff --git a/core/java/android/ddm/DdmRegister.java b/core/java/android/ddm/DdmRegister.java index debf189..ecd450d 100644 --- a/core/java/android/ddm/DdmRegister.java +++ b/core/java/android/ddm/DdmRegister.java @@ -17,7 +17,6 @@ package android.ddm; import org.apache.harmony.dalvik.ddmc.DdmServer; -import android.util.Config; import android.util.Log; /** @@ -44,7 +43,7 @@ public class DdmRegister { * we finish here. */ public static void registerHandlers() { - if (Config.LOGV) + if (false) Log.v("ddm", "Registering DDM message handlers"); DdmHandleHello.register(); DdmHandleThread.register(); diff --git a/core/java/android/gesture/Gesture.java b/core/java/android/gesture/Gesture.java index 300cd28..c6a2a87 100755 --- a/core/java/android/gesture/Gesture.java +++ b/core/java/android/gesture/Gesture.java @@ -36,7 +36,7 @@ import java.util.concurrent.atomic.AtomicInteger; /** * A gesture is a hand-drawn shape on a touch screen. It can have one or multiple strokes. * Each stroke is a sequence of timed points. A user-defined gesture can be recognized by - * a GestureLibrary and a built-in alphabet gesture can be recognized by a LetterRecognizer. + * a GestureLibrary. */ public class Gesture implements Parcelable { diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 97f0e1b..1df3108 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -16,21 +16,22 @@ package android.hardware; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.StringTokenizer; -import java.io.IOException; - -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; import android.os.Message; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.StringTokenizer; /** * The Camera class is used to set image capture settings, start/stop preview, @@ -172,16 +173,16 @@ public class Camera { public int facing; /** - * The orientation of the camera image. The value is the angle that the + * <p>The orientation of the camera image. The value is the angle that the * camera image needs to be rotated clockwise so it shows correctly on - * the display in its natural orientation. It should be 0, 90, 180, or 270. + * the display in its natural orientation. It should be 0, 90, 180, or 270.</p> * - * For example, suppose a device has a naturally tall screen. The + * <p>For example, suppose a device has a naturally tall screen. The * back-facing camera sensor is mounted in landscape. You are looking at * the screen. If the top side of the camera sensor is aligned with the * right edge of the screen in natural orientation, the value should be * 90. If the top side of a front-facing camera sensor is aligned with - * the right of the screen, the value should be 270. + * the right of the screen, the value should be 270.</p> * * @see #setDisplayOrientation(int) * @see Parameters#setRotation(int) @@ -374,6 +375,12 @@ public class Camera { * The preview surface texture may not otherwise change while preview is * running. * + * <p>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 @@ -553,12 +561,12 @@ public class Camera { * is used while calling {@link #takePicture(Camera.ShutterCallback, * Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)}. * - * Please note that by calling this method, the mode for application-managed - * callback buffers is triggered. If this method has never been called, - * null will be returned by the raw image callback since there is - * no image callback buffer available. Furthermore, When a supplied buffer - * is too small to hold the raw image data, raw image callback will return - * null and the buffer will be removed from the buffer queue. + * <p>Please note that by calling this method, the mode for + * application-managed callback buffers is triggered. If this method has + * never been called, null will be returned by the raw image callback since + * there is no image callback buffer available. Furthermore, When a supplied + * buffer is too small to hold the raw image data, raw image callback will + * return null and the buffer will be removed from the buffer queue. * * @param callbackBuffer the buffer to add to the raw image callback buffer * queue. The size should be width * height * (bits per pixel) / 8. An @@ -826,8 +834,6 @@ public class Camera { * @param raw the callback for raw (uncompressed) image data, or null * @param postview callback with postview image data, may be null * @param jpeg the callback for JPEG image data, or null - * - * @see #addRawImageCallbackBuffer(byte[]) */ public final void takePicture(ShutterCallback shutter, PictureCallback raw, PictureCallback postview, PictureCallback jpeg) { @@ -1076,6 +1082,94 @@ public class Camera { }; /** + * <p>The Area class is used for choosing specific metering and focus areas for + * the camera to use when calculating auto-exposure, auto-white balance, and + * auto-focus.</p> + * + * <p>To find out how many simultaneous areas a given camera supports, use + * {@link Parameters#getMaxNumMeteringAreas()} and + * {@link Parameters#getMaxNumFocusAreas()}. If metering or focusing area + * selection is unsupported, these methods will return 0.</p> + * + * <p>Each Area consists of a rectangle specifying its bounds, and a weight + * that determines its importance. The bounds are relative to the camera's + * current field of view. The coordinates are mapped so that (-1000, -1000) + * is always the top-left corner of the current field of view, and (1000, + * 1000) is always the bottom-right corner of the current field of + * view. Setting Areas with bounds outside that range is not allowed. Areas + * with zero or negative width or height are not allowed.</p> + * + * <p>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 overlap and the driver + * will add the weights in the overlap region.</p> + * + * @see Parameters#setFocusAreas(List) + * @see Parameters#getFocusAreas() + * @see Parameters#getMaxNumFocusAreas() + * @see Parameters#setMeteringAreas(List) + * @see Parameters#getMeteringAreas() + * @see Parameters#getMaxNumMeteringAreas() + */ + public static class Area { + /** + * Create an area with specified rectangle and weight. + * + * @param rect the bounds 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; + } + + /** + * Bounds of the area. (-1000, -1000) represents the top-left of the + * camera field of view, and (1000, 1000) represents the bottom-right of + * the field of view. Setting bounds outside that range is not + * allowed. Bounds with zero or negative width or height are not + * allowed. + * + * @see Parameters#getFocusAreas() + * @see Parameters#getMeteringAreas() + */ + public Rect rect; + + /** + * Weight of the area. 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 overlap and + * the driver will add the weights in the overlap region. + * + * @see Parameters#getFocusAreas() + * @see Parameters#getMeteringAreas() + */ + public int weight; + } + + /** * Camera service settings. * * <p>To make camera parameters take effect, applications have to call @@ -1117,6 +1211,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 +1220,12 @@ public class Camera { private static final String KEY_MAX_EXPOSURE_COMPENSATION = "max-exposure-compensation"; private static final String KEY_MIN_EXPOSURE_COMPENSATION = "min-exposure-compensation"; private static final String KEY_EXPOSURE_COMPENSATION_STEP = "exposure-compensation-step"; + private static final String KEY_AUTO_EXPOSURE_LOCK = "auto-exposure-lock"; + private static final String KEY_AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported"; + private static final String KEY_AUTO_WHITEBALANCE_LOCK = "auto-whitebalance-lock"; + private static final String KEY_AUTO_WHITEBALANCE_LOCK_SUPPORTED = "auto-whitebalance-lock-supported"; + private static final String KEY_METERING_AREAS = "metering-areas"; + private static final String KEY_MAX_NUM_METERING_AREAS = "max-num-metering-areas"; private static final String KEY_ZOOM = "zoom"; private static final String KEY_MAX_ZOOM = "max-zoom"; private static final String KEY_ZOOM_RATIOS = "zoom-ratios"; @@ -1138,6 +1240,7 @@ public class Camera { private static final String SUPPORTED_VALUES_SUFFIX = "-values"; private static final String TRUE = "true"; + private static final String FALSE = "false"; // Values for white balance settings. public static final String WHITE_BALANCE_AUTO = "auto"; @@ -1462,6 +1565,31 @@ public class Camera { mMap.put(key, Integer.toString(value)); } + private void set(String key, List<Area> areas) { + if (areas == null) { + set(key, "(0,0,0,0,0)"); + } else { + StringBuilder buffer = new StringBuilder(); + for (int i = 0; i < areas.size(); i++) { + Area area = areas.get(i); + Rect rect = area.rect; + buffer.append('('); + buffer.append(rect.left); + buffer.append(','); + buffer.append(rect.top); + buffer.append(','); + buffer.append(rect.right); + buffer.append(','); + buffer.append(rect.bottom); + buffer.append(','); + buffer.append(area.weight); + buffer.append(')'); + if (i != areas.size() - 1) buffer.append(','); + } + set(key, buffer.toString()); + } + } + /** * Returns the value of a String parameter. * @@ -1483,7 +1611,9 @@ public class Camera { } /** - * Sets the dimensions for preview pictures. + * Sets the dimensions for preview pictures. If the preview has already + * started, applications should stop the preview first before changing + * preview size. * * The sides of width and height are based on camera orientation. That * is, the preview size is the size before it is rotated by display @@ -1531,15 +1661,15 @@ public class Camera { } /** - * Gets the supported video frame sizes that can be used by - * MediaRecorder. + * <p>Gets the supported video frame sizes that can be used by + * MediaRecorder.</p> * - * If the returned list is not null, the returned list will contain at + * <p>If the returned list is not null, the returned list will contain at * least one Size and one of the sizes in the returned list must be * passed to MediaRecorder.setVideoSize() for camcorder application if * camera is used as the video source. In this case, the size of the * preview can be different from the resolution of the recorded video - * during video recording. + * during video recording.</p> * * @return a list of Size object if camera has separate preview and * video output; otherwise, null is returned. @@ -1571,12 +1701,12 @@ public class Camera { } /** - * Sets the dimensions for EXIF thumbnail in Jpeg picture. If + * <p>Sets the dimensions for EXIF thumbnail in Jpeg picture. If * applications set both width and height to 0, EXIF will not contain - * thumbnail. + * thumbnail.</p> * - * Applications need to consider the display orientation. See {@link - * #setPreviewSize(int,int)} for reference. + * <p>Applications need to consider the display orientation. See {@link + * #setPreviewSize(int,int)} for reference.</p> * * @param width the width of the thumbnail, in pixels * @param height the height of the thumbnail, in pixels @@ -1796,10 +1926,10 @@ public class Camera { } /** - * Sets the dimensions for pictures. + * <p>Sets the dimensions for pictures.</p> * - * Applications need to consider the display orientation. See {@link - * #setPreviewSize(int,int)} for reference. + * <p>Applications need to consider the display orientation. See {@link + * #setPreviewSize(int,int)} for reference.</p> * * @param width the width for pictures, in pixels * @param height the height for pictures, in pixels @@ -2381,6 +2511,175 @@ public class Camera { } /** + * <p>Sets the auto-exposure lock state. Applications should check + * {@link #isAutoExposureLockSupported} before using this method.</p> + * + * <p>If set to true, the camera auto-exposure routine will immediately + * pause until the lock is set to false. Exposure compensation settings + * changes will still take effect while auto-exposure is locked.</p> + * + * <p>If auto-exposure is already locked, setting this to true again has + * no effect (the driver will not recalculate exposure values).</p> + * + * <p>Stopping preview with {@link #stopPreview()}, or triggering still + * image capture with {@link #takePicture(Camera.ShutterCallback, + * Camera.PictureCallback, Camera.PictureCallback)}, will automatically + * set the lock to false. However, the lock can be re-enabled before + * preview is re-started to keep the same AE parameters.</p> + * + * <p>Exposure compensation, in conjunction with re-enabling the AE and + * AWB locks after each still capture, can be used to capture an + * exposure-bracketed burst of images, for example.</p> + * + * <p>Auto-exposure state, including the lock state, will not be + * maintained after camera {@link #release()} is called. Locking + * auto-exposure after {@link #open()} but before the first call to + * {@link #startPreview()} will not allow the auto-exposure routine to + * run at all, and may result in severely over- or under-exposed + * images.</p> + * + * <p>The driver may also independently lock auto-exposure after + * auto-focus completes. If this is undesirable, be sure to always set + * the auto-exposure lock to false after the + * {@link AutoFocusCallback#onAutoFocus(boolean, Camera)} callback is + * received. The {@link #getAutoExposureLock()} method can be used after + * the callback to determine if the camera has locked auto-exposure + * independently.</p> + * + * @param toggle new state of the auto-exposure lock. True means that + * auto-exposure is locked, false means that the auto-exposure + * routine is free to run normally. + * + * @see #getAutoExposureLock() + * + * @hide + */ + public void setAutoExposureLock(boolean toggle) { + set(KEY_AUTO_EXPOSURE_LOCK, toggle ? TRUE : FALSE); + } + + /** + * Gets the state of the auto-exposure lock. Applications should check + * {@link #isAutoExposureLockSupported} before using this method. See + * {@link #setAutoExposureLock} for details about the lock. + * + * @return State of the auto-exposure lock. Returns true if + * auto-exposure is currently locked, and false otherwise. The + * auto-exposure lock may be independently enabled by the camera + * subsystem when auto-focus has completed. This method can be + * used after the {@link AutoFocusCallback#onAutoFocus(boolean, + * Camera)} callback to determine if the camera has locked AE. + * + * @see #setAutoExposureLock(boolean) + * + * @hide + */ + public boolean getAutoExposureLock() { + String str = get(KEY_AUTO_EXPOSURE_LOCK); + return TRUE.equals(str); + } + + /** + * Returns true if auto-exposure locking is supported. Applications + * should call this before trying to lock auto-exposure. See + * {@link #setAutoExposureLock} for details about the lock. + * + * @return true if auto-exposure lock is supported. + * @see #setAutoExposureLock(boolean) + * + * @hide + */ + public boolean isAutoExposureLockSupported() { + String str = get(KEY_AUTO_EXPOSURE_LOCK_SUPPORTED); + return TRUE.equals(str); + } + + /** + * <p>Sets the auto-white balance lock state. Applications should check + * {@link #isAutoWhiteBalanceLockSupported} before using this + * method.</p> + * + * <p>If set to true, the camera auto-white balance routine will + * immediately pause until the lock is set to false.</p> + * + * <p>If auto-white balance is already locked, setting this to true + * again has no effect (the driver will not recalculate white balance + * values).</p> + * + * <p>Stopping preview with {@link #stopPreview()}, or triggering still + * image capture with {@link #takePicture(Camera.ShutterCallback, + * Camera.PictureCallback, Camera.PictureCallback)}, will automatically + * set the lock to false. However, the lock can be re-enabled before + * preview is re-started to keep the same white balance parameters.</p> + * + * <p>Exposure compensation, in conjunction with re-enabling the AE and + * AWB locks after each still capture, can be used to capture an + * exposure-bracketed burst of images, for example. Auto-white balance + * state, including the lock state, will not be maintained after camera + * {@link #release()} is called. Locking auto-white balance after + * {@link #open()} but before the first call to {@link #startPreview()} + * will not allow the auto-white balance routine to run at all, and may + * result in severely incorrect color in captured images.</p> + * + * <p>The driver may also independently lock auto-white balance after + * auto-focus completes. If this is undesirable, be sure to always set + * the auto-white balance lock to false after the + * {@link AutoFocusCallback#onAutoFocus(boolean, Camera)} callback is + * received. The {@link #getAutoWhiteBalanceLock()} method can be used + * after the callback to determine if the camera has locked auto-white + * balance independently.</p> + * + * @param toggle new state of the auto-white balance lock. True means + * that auto-white balance is locked, false means that the + * auto-white balance routine is free to run normally. + * + * @see #getAutoWhiteBalanceLock() + * + * @hide + */ + public void setAutoWhiteBalanceLock(boolean toggle) { + set(KEY_AUTO_WHITEBALANCE_LOCK, toggle ? TRUE : FALSE); + } + + /** + * Gets the state of the auto-white balance lock. Applications should + * check {@link #isAutoWhiteBalanceLockSupported} before using this + * method. See {@link #setAutoWhiteBalanceLock} for details about the + * lock. + * + * @return State of the auto-white balance lock. Returns true if + * auto-white balance is currently locked, and false + * otherwise. The auto-white balance lock may be independently + * enabled by the camera subsystem when auto-focus has + * completed. This method can be used after the + * {@link AutoFocusCallback#onAutoFocus(boolean, Camera)} + * callback to determine if the camera has locked AWB. + * + * @see #setAutoWhiteBalanceLock(boolean) + * + * @hide + */ + public boolean getAutoWhiteBalanceLock() { + String str = get(KEY_AUTO_WHITEBALANCE_LOCK); + return TRUE.equals(str); + } + + /** + * Returns true if auto-white balance locking is supported. Applications + * should call this before trying to lock auto-white balance. See + * {@link #setAutoWhiteBalanceLock} for details about the lock. + * + * @return true if auto-white balance lock is supported. + * @see #setAutoWhiteBalanceLock(boolean) + * + * @hide + */ + public boolean isAutoWhiteBalanceLockSupported() { + String str = get(KEY_AUTO_WHITEBALANCE_LOCK_SUPPORTED); + return TRUE.equals(str); + } + + /** * Gets current zoom value. This also works when smooth zoom is in * progress. Applications should check {@link #isZoomSupported} before * using this method. @@ -2456,26 +2755,26 @@ public class Camera { } /** - * Gets the distances from the camera to where an object appears to be + * <p>Gets the distances from the camera to where an object appears to be * in focus. The object is sharpest at the optimal focus distance. The - * depth of field is the far focus distance minus near focus distance. + * depth of field is the far focus distance minus near focus distance.</p> * - * Focus distances may change after calling {@link + * <p>Focus distances may change after calling {@link * #autoFocus(AutoFocusCallback)}, {@link #cancelAutoFocus}, or {@link * #startPreview()}. Applications can call {@link #getParameters()} * and this method anytime to get the latest focus distances. If the * focus mode is FOCUS_MODE_CONTINUOUS_VIDEO, focus distances may change - * from time to time. + * from time to time.</p> * - * This method is intended to estimate the distance between the camera + * <p>This method is intended to estimate the distance between the camera * and the subject. After autofocus, the subject distance may be within * near and far focus distance. However, the precision depends on the * camera hardware, autofocus algorithm, the focus area, and the scene. - * The error can be large and it should be only used as a reference. + * The error can be large and it should be only used as a reference.</p> * - * Far focus distance >= optimal focus distance >= near focus distance. + * <p>Far focus distance >= optimal focus distance >= near focus distance. * If the focus distance is infinity, the value will be - * Float.POSITIVE_INFINITY. + * {@code Float.POSITIVE_INFINITY}.</p> * * @param output focus distances in meters. output must be a float * array with three elements. Near focus distance, optimal focus @@ -2492,6 +2791,140 @@ public class Camera { splitFloat(get(KEY_FOCUS_DISTANCES), output); } + /** + * Gets the maximum number of focus areas supported. This is the maximum + * length of the list in {@link #setFocusAreas(List)} and + * {@link #getFocusAreas()}. + * + * @return the maximum number of focus areas supported by the camera. + * @see #getFocusAreas() + */ + public int getMaxNumFocusAreas() { + return getInt(KEY_MAX_NUM_FOCUS_AREAS, 0); + } + + /** + * <p>Gets the current focus areas. Camera driver uses the areas to decide + * focus.</p> + * + * <p>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.</p> + * + * <p>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 width and height of focus + * areas cannot be 0 or negative.</p> + * + * <p>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.</p> + * + * <p>A special case of a {@code null} focus area list means the driver is + * free to select focus targets as it wants. For example, the driver may + * use more signals to select focus areas and change them + * dynamically. Apps can set the focus area list to {@code null} if they + * want the driver to completely control focusing.</p> + * + * <p>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.</p> + * + * <p>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}.</p> + * + * @return a list of current focus areas + */ + public List<Area> getFocusAreas() { + return splitArea(get(KEY_FOCUS_AREAS)); + } + + /** + * Sets focus areas. See {@link #getFocusAreas()} for documentation. + * + * @param focusAreas the focus areas + * @see #getFocusAreas() + */ + public void setFocusAreas(List<Area> focusAreas) { + set(KEY_FOCUS_AREAS, focusAreas); + } + + /** + * Gets the maximum number of metering areas supported. This is the + * maximum length of the list in {@link #setMeteringAreas(List)} and + * {@link #getMeteringAreas()}. + * + * @return the maximum number of metering areas supported by the camera. + * @see #getMeteringAreas() + */ + public int getMaxNumMeteringAreas() { + return getInt(KEY_MAX_NUM_METERING_AREAS, 0); + } + + /** + * <p>Gets the current metering areas. Camera driver uses these areas to + * decide exposure.</p> + * + * <p>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.</p> + * + * <p>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 width and height of + * metering areas cannot be 0 or negative.</p> + * + * <p>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.</p> + * + * <p>A special case of a {@code null} metering area list means the driver + * is free to meter as it chooses. For example, the driver may use more + * signals to select metering areas and change them dynamically. Apps + * can set the metering area list to {@code null} if they want the + * driver to completely control metering.</p> + * + * <p>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.</p> + * + * <p>No matter what metering areas are, the final exposure are compensated + * by {@link #setExposureCompensation(int)}.</p> + * + * @return a list of current metering areas + */ + public List<Area> getMeteringAreas() { + return splitArea(KEY_METERING_AREAS); + } + + /** + * Sets metering areas. See {@link #getMeteringAreas()} for + * documentation. + * + * @param meteringAreas the metering areas + * @see #getMeteringAreas() + */ + public void setMeteringAreas(List<Area> meteringAreas) { + set(KEY_METERING_AREAS, meteringAreas); + } + // Splits a comma delimited string to an ArrayList of String. // Return null if the passing string is null or the size is 0. private ArrayList<String> split(String str) { @@ -2617,5 +3050,41 @@ public class Camera { if (rangeList.size() == 0) return null; return rangeList; } + + // Splits a comma delimited string to an ArrayList of Area objects. + // Example string: "(-10,-10,0,0,300),(0,0,10,10,700)". Return null if + // the passing string is null or the size is 0 or (0,0,0,0,0). + private ArrayList<Area> splitArea(String str) { + if (str == null || str.charAt(0) != '(' + || str.charAt(str.length() - 1) != ')') { + Log.e(TAG, "Invalid area string=" + str); + return null; + } + + ArrayList<Area> result = new ArrayList<Area>(); + int endIndex, fromIndex = 1; + int[] array = new int[5]; + do { + endIndex = str.indexOf("),(", fromIndex); + if (endIndex == -1) endIndex = str.length() - 1; + splitInt(str.substring(fromIndex, endIndex), array); + Rect rect = new Rect(array[0], array[1], array[2], array[3]); + result.add(new Area(rect, array[4])); + fromIndex = endIndex + 3; + } while (endIndex != str.length() - 1); + + if (result.size() == 0) return null; + + if (result.size() == 1) { + Area area = result.get(0); + Rect rect = area.rect; + if (rect.left == 0 && rect.top == 0 && rect.right == 0 + && rect.bottom == 0 && area.weight == 0) { + return null; + } + } + + return result; + } }; } diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java index 595c7d1..68fc101 100644 --- a/core/java/android/hardware/Sensor.java +++ b/core/java/android/hardware/Sensor.java @@ -66,7 +66,14 @@ public class Sensor { /** A constant describing a pressure sensor type */ public static final int TYPE_PRESSURE = 6; - /** A constant describing a temperature sensor type */ + /** + * A constant describing a temperature sensor type + * + * @deprecated use + * {@link android.hardware.Sensor#TYPE_AMBIENT_TEMPERATURE + * Sensor.TYPE_AMBIENT_TEMPERATURE} instead. + */ + @Deprecated public static final int TYPE_TEMPERATURE = 7; /** @@ -104,6 +111,9 @@ public class Sensor { */ public static final int TYPE_RELATIVE_HUMIDITY = 12; + /** A constant describing an ambient temperature sensor type */ + public static final int TYPE_AMBIENT_TEMPERATURE = 13; + /** * A constant describing all sensor types. */ diff --git a/core/java/android/hardware/SensorEvent.java b/core/java/android/hardware/SensorEvent.java index 9b62a68..0411b5c 100644 --- a/core/java/android/hardware/SensorEvent.java +++ b/core/java/android/hardware/SensorEvent.java @@ -204,6 +204,12 @@ public class SensorEvent { * values[0]: Ambient light level in SI lux units * </ul> * + * <h4>{@link android.hardware.Sensor#TYPE_PRESSURE Sensor.TYPE_PRESSURE}:</h4> + * <ul> + * <p> + * values[0]: Atmospheric pressure in hPa (millibar) + * </ul> + * * <h4>{@link android.hardware.Sensor#TYPE_PROXIMITY Sensor.TYPE_PROXIMITY}: * </h4> * @@ -247,6 +253,23 @@ public class SensorEvent { * <p>Elements of the rotation vector are unitless. * The x,y, and z axis are defined in the same way as the acceleration * sensor.</p> + * The reference coordinate system is defined as a direct orthonormal basis, + * where: + * </p> + * + * <ul> + * <li>X is defined as the vector product <b>Y.Z</b> (It is tangential to + * the ground at the device's current location and roughly points East).</li> + * <li>Y is tangential to the ground at the device's current location and + * points towards the magnetic North Pole.</li> + * <li>Z points towards the sky and is perpendicular to the ground.</li> + * </ul> + * + * <p> + * <center><img src="../../../images/axis_globe.png" + * alt="World coordinate-system diagram." border="0" /></center> + * </p> + * * <ul> * <p> * values[0]: x*sin(θ/2) @@ -362,6 +385,14 @@ public class SensorEvent { * dv = 216.7 * * (rh / 100.0 * 6.112 * Math.exp(17.62 * t / (243.12 + t)) / (273.15 + t)); * </pre> + * + * <h4>{@link android.hardware.Sensor#TYPE_AMBIENT_TEMPERATURE Sensor.TYPE_AMBIENT_TEMPERATURE}: + * </h4> + * + * <ul> + * <p> + * values[0]: ambient (room) temperature in degree Celsius. + * </ul> * * @see SensorEvent * @see GeomagneticField diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java index 60b37a1..5994c98 100644 --- a/core/java/android/hardware/usb/UsbManager.java +++ b/core/java/android/hardware/usb/UsbManager.java @@ -50,11 +50,20 @@ public class UsbManager { * This is a sticky broadcast for clients that includes USB connected/disconnected state, * <ul> * <li> {@link #USB_CONNECTED} boolean indicating whether USB is connected or disconnected. - * <li> {@link #USB_CONFIGURATION} a Bundle containing name/value pairs where the name - * is the name of a USB function and the value is either {@link #USB_FUNCTION_ENABLED} - * or {@link #USB_FUNCTION_DISABLED}. The possible function names include - * {@link #USB_FUNCTION_MASS_STORAGE}, {@link #USB_FUNCTION_ADB}, {@link #USB_FUNCTION_RNDIS}, - * {@link #USB_FUNCTION_MTP} and {@link #USB_FUNCTION_ACCESSORY}. + * <li> {@link #USB_CONFIGURATION} integer containing current USB configuration + * currently zero if not configured, one for configured. + * <li> {@link #USB_FUNCTION_MASS_STORAGE} boolean extra indicating whether the + * mass storage function is enabled + * <li> {@link #USB_FUNCTION_ADB} boolean extra indicating whether the + * adb function is enabled + * <li> {@link #USB_FUNCTION_RNDIS} boolean extra indicating whether the + * RNDIS ethernet function is enabled + * <li> {@link #USB_FUNCTION_MTP} boolean extra indicating whether the + * MTP function is enabled + * <li> {@link #USB_FUNCTION_PTP} boolean extra indicating whether the + * PTP function is enabled + * <li> {@link #USB_FUNCTION_PTP} boolean extra indicating whether the + * accessory function is enabled * </ul> * * {@hide} @@ -159,30 +168,20 @@ public class UsbManager { public static final String USB_FUNCTION_MTP = "mtp"; /** - * Name of the Accessory USB function. + * Name of the PTP USB function. * Used in extras for the {@link #ACTION_USB_STATE} broadcast * * {@hide} */ - public static final String USB_FUNCTION_ACCESSORY = "accessory"; - - /** - * Value indicating that a USB function is enabled. - * Used in {@link #USB_CONFIGURATION} extras bundle for the - * {@link #ACTION_USB_STATE} broadcast - * - * {@hide} - */ - public static final String USB_FUNCTION_ENABLED = "enabled"; + public static final String USB_FUNCTION_PTP = "ptp"; /** - * Value indicating that a USB function is disabled. - * Used in {@link #USB_CONFIGURATION} extras bundle for the - * {@link #ACTION_USB_STATE} broadcast + * Name of the Accessory USB function. + * Used in extras for the {@link #ACTION_USB_STATE} broadcast * * {@hide} */ - public static final String USB_FUNCTION_DISABLED = "disabled"; + public static final String USB_FUNCTION_ACCESSORY = "accessory"; /** * Name of extra for {@link #ACTION_USB_DEVICE_ATTACHED} and diff --git a/core/java/android/inputmethodservice/ExtractButton.java b/core/java/android/inputmethodservice/ExtractButton.java index f91cd4e..b6b7595 100644 --- a/core/java/android/inputmethodservice/ExtractButton.java +++ b/core/java/android/inputmethodservice/ExtractButton.java @@ -41,6 +41,6 @@ class ExtractButton extends Button { * highlight when selected. */ @Override public boolean hasWindowFocus() { - return this.isEnabled() ? true : false; + return isEnabled() && getVisibility() == VISIBLE ? true : false; } } diff --git a/core/java/android/inputmethodservice/ExtractEditLayout.java b/core/java/android/inputmethodservice/ExtractEditLayout.java new file mode 100644 index 0000000..eafff49 --- /dev/null +++ b/core/java/android/inputmethodservice/ExtractEditLayout.java @@ -0,0 +1,183 @@ +/* + * 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.inputmethodservice; + +import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.view.menu.MenuPopupHelper; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; + +/** + * ExtractEditLayout provides an ActionMode presentation for the + * limited screen real estate in extract mode. + * + * @hide + */ +public class ExtractEditLayout extends LinearLayout { + ExtractActionMode mActionMode; + Button mExtractActionButton; + Button mEditButton; + + public ExtractEditLayout(Context context) { + super(context); + } + + public ExtractEditLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public ActionMode startActionModeForChild(View sourceView, ActionMode.Callback cb) { + final ExtractActionMode mode = new ExtractActionMode(cb); + if (mode.dispatchOnCreate()) { + mode.invalidate(); + mExtractActionButton.setVisibility(INVISIBLE); + mEditButton.setVisibility(VISIBLE); + mActionMode = mode; + return mode; + } + return null; + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + mExtractActionButton = (Button) findViewById(com.android.internal.R.id.inputExtractAction); + mEditButton = (Button) findViewById(com.android.internal.R.id.inputExtractEditButton); + mEditButton.setOnClickListener(new OnClickListener() { + public void onClick(View clicked) { + if (mActionMode != null) { + new MenuPopupHelper(getContext(), mActionMode.mMenu, clicked).show(); + } + } + }); + } + + private class ExtractActionMode extends ActionMode implements MenuBuilder.Callback { + private ActionMode.Callback mCallback; + MenuBuilder mMenu; + + public ExtractActionMode(Callback cb) { + mMenu = new MenuBuilder(getContext()); + mMenu.setCallback(this); + mCallback = cb; + } + + @Override + public void setTitle(CharSequence title) { + // Title will not be shown. + } + + @Override + public void setTitle(int resId) { + // Title will nor be shown. + } + + @Override + public void setSubtitle(CharSequence subtitle) { + // Subtitle will not be shown. + } + + @Override + public void setSubtitle(int resId) { + // Subtitle will not be shown. + } + + @Override + public void setCustomView(View view) { + // Custom view is not supported here. + } + + @Override + public void invalidate() { + mMenu.stopDispatchingItemsChanged(); + try { + mCallback.onPrepareActionMode(this, mMenu); + } finally { + mMenu.startDispatchingItemsChanged(); + } + } + + public boolean dispatchOnCreate() { + mMenu.stopDispatchingItemsChanged(); + try { + return mCallback.onCreateActionMode(this, mMenu); + } finally { + mMenu.startDispatchingItemsChanged(); + } + } + + @Override + public void finish() { + if (mActionMode != this) { + // Not the active action mode - no-op + return; + } + + mCallback.onDestroyActionMode(this); + mCallback = null; + + mExtractActionButton.setVisibility(VISIBLE); + mEditButton.setVisibility(INVISIBLE); + + mActionMode = null; + } + + @Override + public Menu getMenu() { + return mMenu; + } + + @Override + public CharSequence getTitle() { + return null; + } + + @Override + public CharSequence getSubtitle() { + return null; + } + + @Override + public View getCustomView() { + return null; + } + + @Override + public MenuInflater getMenuInflater() { + return new MenuInflater(getContext()); + } + + @Override + public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { + return mCallback.onActionItemClicked(this, item); + } + + @Override + public void onMenuModeChange(MenuBuilder menu) { + } + + } +} diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index ab5c78a..dfc70ef 100644 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -142,7 +142,8 @@ public class KeyboardView extends View implements View.OnClickListener { private int mPreviewTextSizeLarge; private int mPreviewOffset; private int mPreviewHeight; - private int[] mOffsetInWindow; + // Working variable + private final int[] mCoordinates = new int[2]; private PopupWindow mPopupKeyboard; private View mMiniKeyboardContainer; @@ -152,7 +153,6 @@ public class KeyboardView extends View implements View.OnClickListener { private int mMiniKeyboardOffsetX; private int mMiniKeyboardOffsetY; private Map<Key,View> mMiniKeyboardCache; - private int[] mWindowOffset; private Key[] mKeys; /** Listener for {@link OnKeyboardActionListener}. */ @@ -905,23 +905,19 @@ public class KeyboardView extends View implements View.OnClickListener { mPopupPreviewY = - mPreviewText.getMeasuredHeight(); } mHandler.removeMessages(MSG_REMOVE_PREVIEW); - if (mOffsetInWindow == null) { - mOffsetInWindow = new int[2]; - getLocationInWindow(mOffsetInWindow); - mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero - mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero - int[] mWindowLocation = new int[2]; - getLocationOnScreen(mWindowLocation); - mWindowY = mWindowLocation[1]; - } + getLocationInWindow(mCoordinates); + mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero + mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero + // Set the preview background state mPreviewText.getBackground().setState( key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET); - mPopupPreviewX += mOffsetInWindow[0]; - mPopupPreviewY += mOffsetInWindow[1]; + mPopupPreviewX += mCoordinates[0]; + mPopupPreviewY += mCoordinates[1]; // If the popup cannot be shown above the key, put it on the side - if (mPopupPreviewY + mWindowY < 0) { + getLocationOnScreen(mCoordinates); + if (mPopupPreviewY + mCoordinates[1] < 0) { // If the key you're pressing is on the left side of the keyboard, show the popup on // the right, offset by enough to see at least one key to the left/right. if (key.x + key.width <= getWidth() / 2) { @@ -1057,16 +1053,13 @@ public class KeyboardView extends View implements View.OnClickListener { mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById( com.android.internal.R.id.keyboardView); } - if (mWindowOffset == null) { - mWindowOffset = new int[2]; - getLocationInWindow(mWindowOffset); - } + getLocationInWindow(mCoordinates); mPopupX = popupKey.x + mPaddingLeft; mPopupY = popupKey.y + mPaddingTop; mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth(); mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight(); - final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0]; - final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1]; + final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mCoordinates[0]; + final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mCoordinates[1]; mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y); mMiniKeyboard.setShifted(isShifted()); mPopupKeyboard.setContentView(mMiniKeyboardContainer); diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java index 343242e..7159260 100644 --- a/core/java/android/inputmethodservice/SoftInputWindow.java +++ b/core/java/android/inputmethodservice/SoftInputWindow.java @@ -73,8 +73,17 @@ class SoftInputWindow extends Dialog { @Override public boolean dispatchTouchEvent(MotionEvent ev) { getWindow().getDecorView().getHitRect(mBounds); - final MotionEvent event = clipMotionEvent(ev, mBounds); - return super.dispatchTouchEvent(event); + + if (ev.isWithinBoundsNoHistory(mBounds.left, mBounds.top, + mBounds.right - 1, mBounds.bottom - 1)) { + return super.dispatchTouchEvent(ev); + } else { + MotionEvent temp = ev.clampNoHistory(mBounds.left, mBounds.top, + mBounds.right - 1, mBounds.bottom - 1); + boolean handled = super.dispatchTouchEvent(temp); + temp.recycle(); + return handled; + } } /** @@ -163,48 +172,4 @@ class SoftInputWindow extends Dialog { WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_DIM_BEHIND); } - - private static MotionEvent clipMotionEvent(MotionEvent me, Rect bounds) { - final int pointerCount = me.getPointerCount(); - boolean shouldClip = false; - for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) { - final int x = (int)me.getX(pointerIndex); - final int y = (int)me.getY(pointerIndex); - if (!bounds.contains(x, y)) { - shouldClip = true; - break; - } - } - if (!shouldClip) - return me; - - if (pointerCount == 1) { - final int x = (int)me.getX(); - final int y = (int)me.getY(); - me.setLocation( - Math.max(bounds.left, Math.min(x, bounds.right - 1)), - Math.max(bounds.top, Math.min(y, bounds.bottom - 1))); - return me; - } - - final int[] pointerIds = new int[pointerCount]; - final MotionEvent.PointerCoords[] pointerCoords = - new MotionEvent.PointerCoords[pointerCount]; - for (int pointerIndex = 0; pointerIndex < pointerCount; pointerIndex++) { - pointerIds[pointerIndex] = me.getPointerId(pointerIndex); - final MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); - me.getPointerCoords(pointerIndex, coords); - pointerCoords[pointerIndex] = coords; - final int x = (int)coords.x; - final int y = (int)coords.y; - if (!bounds.contains(x, y)) { - coords.x = Math.max(bounds.left, Math.min(x, bounds.right - 1)); - coords.y = Math.max(bounds.top, Math.min(y, bounds.bottom - 1)); - } - } - return MotionEvent.obtain( - me.getDownTime(), me.getEventTime(), me.getAction(), pointerCount, pointerIds, - pointerCoords, me.getMetaState(), me.getXPrecision(), me.getYPrecision(), - me.getDeviceId(), me.getEdgeFlags(), me.getSource(), me.getFlags()); - } } diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index eaf9191..3025462 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -19,10 +19,11 @@ package android.net; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.os.Binder; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import java.net.InetAddress; -import java.net.UnknownHostException; /** * Class that answers queries about the state of network connectivity. It also @@ -40,8 +41,9 @@ import java.net.UnknownHostException; * state of the available networks</li> * </ol> */ -public class ConnectivityManager -{ +public class ConnectivityManager { + private static final String TAG = "ConnectivityManager"; + /** * A change in network connectivity has occurred. A connection has either * been established or lost. The NetworkInfo for the affected network is @@ -109,7 +111,7 @@ public class ConnectivityManager * The lookup key for an int that provides information about * our connection to the internet at large. 0 indicates no connection, * 100 indicates a great connection. Retrieve it with - * {@link android.content.Intent@getIntExtra(String)}. + * {@link android.content.Intent#getIntExtra(String, int)}. * {@hide} */ public static final String EXTRA_INET_CONDITION = "inetCondition"; @@ -120,13 +122,12 @@ public class ConnectivityManager * <p> * If an application uses the network in the background, it should listen * for this broadcast and stop using the background data if the value is - * false. + * {@code false}. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED = "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED"; - /** * Broadcast Action: The network connection may not be good * uses {@code ConnectivityManager.EXTRA_INET_CONDITION} and @@ -216,7 +217,9 @@ public class ConnectivityManager */ 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; /** @@ -224,6 +227,7 @@ public class ConnectivityManager * will use this connection by default. */ public static final int TYPE_ETHERNET = 9; + /** * Over the air Adminstration. * {@hide} @@ -250,12 +254,63 @@ public class ConnectivityManager public static final int DEFAULT_NETWORK_PREFERENCE = TYPE_WIFI; - private IConnectivityManager mService; + private final IConnectivityManager mService; - static public boolean isNetworkTypeValid(int networkType) { + public static boolean isNetworkTypeValid(int networkType) { return networkType >= 0 && networkType <= MAX_NETWORK_TYPE; } + /** {@hide} */ + public static String getNetworkTypeName(int type) { + switch (type) { + case TYPE_MOBILE: + return "MOBILE"; + case TYPE_WIFI: + return "WIFI"; + case TYPE_MOBILE_MMS: + return "MOBILE_MMS"; + case TYPE_MOBILE_SUPL: + return "MOBILE_SUPL"; + case TYPE_MOBILE_DUN: + return "MOBILE_DUN"; + case TYPE_MOBILE_HIPRI: + return "MOBILE_HIPRI"; + case TYPE_WIMAX: + return "WIMAX"; + case TYPE_BLUETOOTH: + return "BLUETOOTH"; + case TYPE_DUMMY: + return "DUMMY"; + case TYPE_ETHERNET: + return "ETHERNET"; + case TYPE_MOBILE_FOTA: + return "MOBILE_FOTA"; + case TYPE_MOBILE_IMS: + return "MOBILE_IMS"; + case TYPE_MOBILE_CBS: + return "MOBILE_CBS"; + default: + return Integer.toString(type); + } + } + + /** {@hide} */ + public static boolean isNetworkTypeMobile(int networkType) { + switch (networkType) { + case TYPE_MOBILE: + case TYPE_MOBILE_MMS: + case TYPE_MOBILE_SUPL: + case TYPE_MOBILE_DUN: + case TYPE_MOBILE_HIPRI: + case TYPE_MOBILE_FOTA: + case TYPE_MOBILE_IMS: + case TYPE_MOBILE_CBS: + return true; + default: + return false; + } + } + public void setNetworkPreference(int preference) { try { mService.setNetworkPreference(preference); @@ -279,6 +334,15 @@ public class ConnectivityManager } } + /** {@hide} */ + public NetworkInfo getActiveNetworkInfoForUid(int uid) { + try { + return mService.getActiveNetworkInfoForUid(uid); + } catch (RemoteException e) { + return null; + } + } + public NetworkInfo getNetworkInfo(int networkType) { try { return mService.getNetworkInfo(networkType); @@ -295,7 +359,7 @@ public class ConnectivityManager } } - /** @hide */ + /** {@hide} */ public LinkProperties getActiveLinkProperties() { try { return mService.getActiveLinkProperties(); @@ -304,7 +368,7 @@ public class ConnectivityManager } } - /** @hide */ + /** {@hide} */ public LinkProperties getLinkProperties(int networkType) { try { return mService.getLinkProperties(networkType); @@ -474,19 +538,11 @@ public class ConnectivityManager } /** - * Don't allow use of default constructor. - */ - @SuppressWarnings({"UnusedDeclaration"}) - private ConnectivityManager() { - } - - /** * {@hide} */ public ConnectivityManager(IConnectivityManager service) { if (service == null) { - throw new IllegalArgumentException( - "ConnectivityManager() cannot be constructed with null service"); + throw new IllegalArgumentException("missing IConnectivityManager"); } mService = service; } @@ -702,4 +758,43 @@ public class ConnectivityManager } catch (RemoteException e) { } } + + /** + * Protect a socket from routing changes. This method is limited to VPN + * applications, and it is always hidden to avoid direct use. + * @hide + */ + public void protectVpn(ParcelFileDescriptor socket) { + try { + mService.protectVpn(socket); + } catch (RemoteException e) { + } + } + + /** + * Prepare for a VPN application. This method is limited to VpnDialogs, + * and it is always hidden to avoid direct use. + * @hide + */ + public String prepareVpn(String packageName) { + try { + return mService.prepareVpn(packageName); + } catch (RemoteException e) { + return null; + } + } + + /** + * Configure a TUN interface and return its file descriptor. Parameters + * are encoded and opaque to this class. This method is limited to VPN + * applications, and it is always hidden to avoid direct use. + * @hide + */ + public ParcelFileDescriptor establishVpn(Bundle config) { + try { + return mService.establishVpn(config); + } catch (RemoteException e) { + return null; + } + } } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 8be492c..7f3775d 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -18,8 +18,11 @@ package android.net; import android.net.LinkProperties; import android.net.NetworkInfo; +import android.net.NetworkState; import android.net.ProxyProperties; +import android.os.Bundle; import android.os.IBinder; +import android.os.ParcelFileDescriptor; /** * Interface that answers queries about, and allows changing, the @@ -33,15 +36,15 @@ interface IConnectivityManager int getNetworkPreference(); NetworkInfo getActiveNetworkInfo(); - + NetworkInfo getActiveNetworkInfoForUid(int uid); NetworkInfo getNetworkInfo(int networkType); - NetworkInfo[] getAllNetworkInfo(); LinkProperties getActiveLinkProperties(); - LinkProperties getLinkProperties(int networkType); + NetworkState[] getAllNetworkState(); + boolean setRadios(boolean onOff); boolean setRadio(int networkType, boolean turnOn); @@ -94,4 +97,10 @@ interface IConnectivityManager ProxyProperties getProxy(); void setDataDependency(int networkType, boolean met); + + void protectVpn(in ParcelFileDescriptor socket); + + String prepareVpn(String packageName); + + ParcelFileDescriptor establishVpn(in Bundle config); } diff --git a/core/java/android/net/INetworkPolicyListener.aidl b/core/java/android/net/INetworkPolicyListener.aidl new file mode 100644 index 0000000..9230151 --- /dev/null +++ b/core/java/android/net/INetworkPolicyListener.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +/** {@hide} */ +oneway interface INetworkPolicyListener { + + void onRulesChanged(int uid, int uidRules); + +} diff --git a/core/java/android/os/INetStatService.aidl b/core/java/android/net/INetworkPolicyManager.aidl index a8f3de0..c1f3530 100644 --- a/core/java/android/os/INetStatService.aidl +++ b/core/java/android/net/INetworkPolicyManager.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 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. @@ -14,22 +14,25 @@ * limitations under the License. */ -package android.os; +package android.net; + +import android.net.INetworkPolicyListener; /** - * Retrieves packet and byte counts for the phone data interface, - * and for all interfaces. - * Used for the data activity icon and the phone status in Settings. + * Interface that creates and modifies network policy rules. * * {@hide} */ -interface INetStatService { - long getMobileTxPackets(); - long getMobileRxPackets(); - long getMobileTxBytes(); - long getMobileRxBytes(); - long getTotalTxPackets(); - long getTotalRxPackets(); - long getTotalTxBytes(); - long getTotalRxBytes(); +interface INetworkPolicyManager { + + void setUidPolicy(int uid, int policy); + int getUidPolicy(int uid); + + boolean isUidForeground(int uid); + + void registerListener(INetworkPolicyListener listener); + void unregisterListener(INetworkPolicyListener listener); + + // TODO: build API to surface stats details for settings UI + } diff --git a/core/java/android/net/INetworkStatsService.aidl b/core/java/android/net/INetworkStatsService.aidl new file mode 100644 index 0000000..d05c9d3 --- /dev/null +++ b/core/java/android/net/INetworkStatsService.aidl @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.net.NetworkStats; +import android.net.NetworkStatsHistory; + +/** {@hide} */ +interface INetworkStatsService { + + /** Return historical stats for traffic that matches template. */ + NetworkStatsHistory getHistoryForNetwork(int networkTemplate); + /** Return historical stats for specific UID traffic that matches template. */ + NetworkStatsHistory getHistoryForUid(int uid, int networkTemplate); + + /** Return usage summary per UID for traffic that matches template. */ + NetworkStats getSummaryForAllUid(long start, long end, int networkTemplate); + +} diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 5f5e11c..537750a 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -74,7 +74,9 @@ public class NetworkInfo implements Parcelable { /** IP traffic not available. */ DISCONNECTED, /** Attempt to connect failed. */ - FAILED + FAILED, + /** Access to this network is blocked. */ + BLOCKED } /** @@ -96,6 +98,7 @@ public class NetworkInfo implements Parcelable { stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING); stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED); stateMap.put(DetailedState.FAILED, State.DISCONNECTED); + stateMap.put(DetailedState.BLOCKED, State.DISCONNECTED); } private int mNetworkType; @@ -138,6 +141,23 @@ public class NetworkInfo implements Parcelable { mIsRoaming = false; } + /** {@hide} */ + public NetworkInfo(NetworkInfo source) { + if (source != null) { + mNetworkType = source.mNetworkType; + mSubtype = source.mSubtype; + mTypeName = source.mTypeName; + mSubtypeName = source.mSubtypeName; + mState = source.mState; + mDetailedState = source.mDetailedState; + mReason = source.mReason; + mExtraInfo = source.mExtraInfo; + mIsFailover = source.mIsFailover; + mIsRoaming = source.mIsRoaming; + mIsAvailable = source.mIsAvailable; + } + } + /** * Reports the type of network (currently mobile or Wi-Fi) to which the * info in this object pertains. diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java new file mode 100644 index 0000000..dd7c1b0 --- /dev/null +++ b/core/java/android/net/NetworkPolicyManager.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.content.Context; +import android.os.RemoteException; + +import java.io.PrintWriter; + +/** + * Manager for creating and modifying network policy rules. + * + * {@hide} + */ +public class NetworkPolicyManager { + + /** No specific network policy, use system default. */ + public static final int POLICY_NONE = 0x0; + /** Reject network usage on paid networks when application in background. */ + public static final int POLICY_REJECT_PAID_BACKGROUND = 0x1; + + /** All network traffic should be allowed. */ + public static final int RULE_ALLOW_ALL = 0x0; + /** Reject traffic on paid networks. */ + public static final int RULE_REJECT_PAID = 0x1; + + private INetworkPolicyManager mService; + + public NetworkPolicyManager(INetworkPolicyManager service) { + if (service == null) { + throw new IllegalArgumentException("missing INetworkPolicyManager"); + } + mService = service; + } + + public static NetworkPolicyManager getSystemService(Context context) { + return (NetworkPolicyManager) context.getSystemService(Context.NETWORK_POLICY_SERVICE); + } + + /** + * Set policy flags for specific UID. + * + * @param policy {@link #POLICY_NONE} or combination of flags like + * {@link #POLICY_REJECT_PAID_BACKGROUND}. + */ + public void setUidPolicy(int uid, int policy) { + try { + mService.setUidPolicy(uid, policy); + } catch (RemoteException e) { + } + } + + public int getUidPolicy(int uid) { + try { + return mService.getUidPolicy(uid); + } catch (RemoteException e) { + return POLICY_NONE; + } + } + + /** {@hide} */ + public static void dumpPolicy(PrintWriter fout, int policy) { + fout.write("["); + if ((policy & POLICY_REJECT_PAID_BACKGROUND) != 0) { + fout.write("REJECT_PAID_BACKGROUND"); + } + fout.write("]"); + } + + /** {@hide} */ + public static void dumpRules(PrintWriter fout, int rules) { + fout.write("["); + if ((rules & RULE_REJECT_PAID) != 0) { + fout.write("REJECT_PAID"); + } + fout.write("]"); + } + +} diff --git a/core/java/android/net/NetworkState.aidl b/core/java/android/net/NetworkState.aidl new file mode 100644 index 0000000..c0b6cdc --- /dev/null +++ b/core/java/android/net/NetworkState.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2011, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +parcelable NetworkState; diff --git a/core/java/android/net/NetworkState.java b/core/java/android/net/NetworkState.java new file mode 100644 index 0000000..749039a --- /dev/null +++ b/core/java/android/net/NetworkState.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Snapshot of network state. + * + * @hide + */ +public class NetworkState implements Parcelable { + + public final NetworkInfo networkInfo; + public final LinkProperties linkProperties; + public final LinkCapabilities linkCapabilities; + + public NetworkState(NetworkInfo networkInfo, LinkProperties linkProperties, + LinkCapabilities linkCapabilities) { + this.networkInfo = networkInfo; + this.linkProperties = linkProperties; + this.linkCapabilities = linkCapabilities; + } + + public NetworkState(Parcel in) { + networkInfo = in.readParcelable(null); + linkProperties = in.readParcelable(null); + linkCapabilities = in.readParcelable(null); + } + + /** {@inheritDoc} */ + public int describeContents() { + return 0; + } + + /** {@inheritDoc} */ + public void writeToParcel(Parcel out, int flags) { + out.writeParcelable(networkInfo, flags); + out.writeParcelable(linkProperties, flags); + out.writeParcelable(linkCapabilities, flags); + } + + public static final Creator<NetworkState> CREATOR = new Creator<NetworkState>() { + public NetworkState createFromParcel(Parcel in) { + return new NetworkState(in); + } + + public NetworkState[] newArray(int size) { + return new NetworkState[size]; + } + }; + +} diff --git a/core/java/android/net/NetworkStats.aidl b/core/java/android/net/NetworkStats.aidl new file mode 100644 index 0000000..d06ca65 --- /dev/null +++ b/core/java/android/net/NetworkStats.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2011, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +parcelable NetworkStats; diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java new file mode 100644 index 0000000..6354e9a --- /dev/null +++ b/core/java/android/net/NetworkStats.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.SparseBooleanArray; + +import java.io.CharArrayWriter; +import java.io.PrintWriter; +import java.util.HashSet; + +/** + * Collection of active network statistics. Can contain summary details across + * all interfaces, or details with per-UID granularity. Internally stores data + * as a large table, closely matching {@code /proc/} data format. This structure + * optimizes for rapid in-memory comparison, but consider using + * {@link NetworkStatsHistory} when persisting. + * + * @hide + */ +public class NetworkStats implements Parcelable { + /** {@link #iface} value when interface details unavailable. */ + public static final String IFACE_ALL = null; + /** {@link #uid} value when UID details unavailable. */ + public static final int UID_ALL = -1; + + // NOTE: data should only be accounted for once in this structure; if data + // is broken out, the summarized version should not be included. + + /** + * {@link SystemClock#elapsedRealtime()} timestamp when this data was + * generated. + */ + public final long elapsedRealtime; + public final String[] iface; + public final int[] uid; + public final long[] rx; + public final long[] tx; + + // TODO: add fg/bg stats once reported by kernel + + private NetworkStats(long elapsedRealtime, String[] iface, int[] uid, long[] rx, long[] tx) { + this.elapsedRealtime = elapsedRealtime; + this.iface = iface; + this.uid = uid; + this.rx = rx; + this.tx = tx; + } + + public NetworkStats(Parcel parcel) { + elapsedRealtime = parcel.readLong(); + iface = parcel.createStringArray(); + uid = parcel.createIntArray(); + rx = parcel.createLongArray(); + tx = parcel.createLongArray(); + } + + public static class Builder { + private long mElapsedRealtime; + private final String[] mIface; + private final int[] mUid; + private final long[] mRx; + private final long[] mTx; + + private int mIndex = 0; + + public Builder(long elapsedRealtime, int size) { + mElapsedRealtime = elapsedRealtime; + mIface = new String[size]; + mUid = new int[size]; + mRx = new long[size]; + mTx = new long[size]; + } + + public Builder addEntry(String iface, int uid, long rx, long tx) { + mIface[mIndex] = iface; + mUid[mIndex] = uid; + mRx[mIndex] = rx; + mTx[mIndex] = tx; + mIndex++; + return this; + } + + public NetworkStats build() { + if (mIndex != mIface.length) { + throw new IllegalArgumentException("unexpected number of entries"); + } + return new NetworkStats(mElapsedRealtime, mIface, mUid, mRx, mTx); + } + } + + public int length() { + // length is identical for all fields + return iface.length; + } + + /** + * Find first stats index that matches the requested parameters. + */ + public int findIndex(String iface, int uid) { + final int length = length(); + for (int i = 0; i < length; i++) { + if (equal(iface, this.iface[i]) && uid == this.uid[i]) { + return i; + } + } + return -1; + } + + /** + * Return list of unique interfaces known by this data structure. + */ + public String[] getUniqueIfaces() { + final HashSet<String> ifaces = new HashSet<String>(); + for (String iface : this.iface) { + if (iface != IFACE_ALL) { + ifaces.add(iface); + } + } + return ifaces.toArray(new String[ifaces.size()]); + } + + /** + * Return list of unique UIDs known by this data structure. + */ + public int[] getUniqueUids() { + final SparseBooleanArray uids = new SparseBooleanArray(); + for (int uid : this.uid) { + uids.put(uid, true); + } + + final int size = uids.size(); + final int[] result = new int[size]; + for (int i = 0; i < size; i++) { + result[i] = uids.keyAt(i); + } + return result; + } + + /** + * Subtract the given {@link NetworkStats}, effectively leaving the delta + * between two snapshots in time. Assumes that statistics rows collect over + * time, and that none of them have disappeared. + * + * @throws IllegalArgumentException when given {@link NetworkStats} is + * non-monotonic. + */ + public NetworkStats subtract(NetworkStats value) { + return subtract(value, true, false); + } + + /** + * Subtract the given {@link NetworkStats}, effectively leaving the delta + * between two snapshots in time. Assumes that statistics rows collect over + * time, and that none of them have disappeared. + * <p> + * Instead of throwing when counters are non-monotonic, this variant clamps + * results to never be negative. + */ + public NetworkStats subtractClamped(NetworkStats value) { + return subtract(value, false, true); + } + + /** + * Subtract the given {@link NetworkStats}, effectively leaving the delta + * between two snapshots in time. Assumes that statistics rows collect over + * time, and that none of them have disappeared. + * + * @param enforceMonotonic Validate that incoming value is strictly + * monotonic compared to this object. + * @param clampNegative Instead of throwing like {@code enforceMonotonic}, + * clamp resulting counters at 0 to prevent negative values. + */ + private NetworkStats subtract( + NetworkStats value, boolean enforceMonotonic, boolean clampNegative) { + final long deltaRealtime = this.elapsedRealtime - value.elapsedRealtime; + if (enforceMonotonic && deltaRealtime < 0) { + throw new IllegalArgumentException("found non-monotonic realtime"); + } + + // result will have our rows, and elapsed time between snapshots + final int length = length(); + final NetworkStats.Builder result = new NetworkStats.Builder(deltaRealtime, length); + for (int i = 0; i < length; i++) { + final String iface = this.iface[i]; + final int uid = this.uid[i]; + + // find remote row that matches, and subtract + final int j = value.findIndex(iface, uid); + if (j == -1) { + // newly appearing row, return entire value + result.addEntry(iface, uid, this.rx[i], this.tx[i]); + } else { + // existing row, subtract remote value + long rx = this.rx[i] - value.rx[j]; + long tx = this.tx[i] - value.tx[j]; + if (enforceMonotonic && (rx < 0 || tx < 0)) { + throw new IllegalArgumentException("found non-monotonic values"); + } + if (clampNegative) { + rx = Math.max(0, rx); + tx = Math.max(0, tx); + } + result.addEntry(iface, uid, rx, tx); + } + } + + return result.build(); + } + + private static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + public void dump(String prefix, PrintWriter pw) { + pw.print(prefix); + pw.print("NetworkStats: elapsedRealtime="); pw.println(elapsedRealtime); + for (int i = 0; i < iface.length; i++) { + pw.print(prefix); + pw.print(" iface="); pw.print(iface[i]); + pw.print(" uid="); pw.print(uid[i]); + pw.print(" rx="); pw.print(rx[i]); + pw.print(" tx="); pw.println(tx[i]); + } + } + + @Override + public String toString() { + final CharArrayWriter writer = new CharArrayWriter(); + dump("", new PrintWriter(writer)); + return writer.toString(); + } + + /** {@inheritDoc} */ + public int describeContents() { + return 0; + } + + /** {@inheritDoc} */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(elapsedRealtime); + dest.writeStringArray(iface); + dest.writeIntArray(uid); + dest.writeLongArray(rx); + dest.writeLongArray(tx); + } + + public static final Creator<NetworkStats> CREATOR = new Creator<NetworkStats>() { + public NetworkStats createFromParcel(Parcel in) { + return new NetworkStats(in); + } + + public NetworkStats[] newArray(int size) { + return new NetworkStats[size]; + } + }; +} diff --git a/core/java/android/net/NetworkStatsHistory.aidl b/core/java/android/net/NetworkStatsHistory.aidl new file mode 100644 index 0000000..8b9069f --- /dev/null +++ b/core/java/android/net/NetworkStatsHistory.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2011, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +parcelable NetworkStatsHistory; diff --git a/core/java/android/net/NetworkStatsHistory.java b/core/java/android/net/NetworkStatsHistory.java new file mode 100644 index 0000000..a697e96 --- /dev/null +++ b/core/java/android/net/NetworkStatsHistory.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.CharArrayWriter; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.ProtocolException; +import java.util.Arrays; +import java.util.Random; + +/** + * Collection of historical network statistics, recorded into equally-sized + * "buckets" in time. Internally it stores data in {@code long} series for more + * efficient persistence. + * <p> + * Each bucket is defined by a {@link #bucketStart} timestamp, and lasts for + * {@link #bucketDuration}. Internally assumes that {@link #bucketStart} is + * sorted at all times. + * + * @hide + */ +public class NetworkStatsHistory implements Parcelable { + private static final int VERSION_CURRENT = 1; + + // TODO: teach about zigzag encoding to use less disk space + // TODO: teach how to convert between bucket sizes + + public final long bucketDuration; + + public int bucketCount; + public long[] bucketStart; + public long[] rx; + public long[] tx; + + public NetworkStatsHistory(long bucketDuration) { + this.bucketDuration = bucketDuration; + bucketStart = new long[0]; + rx = new long[0]; + tx = new long[0]; + bucketCount = bucketStart.length; + } + + public NetworkStatsHistory(Parcel in) { + bucketDuration = in.readLong(); + bucketStart = readLongArray(in); + rx = in.createLongArray(); + tx = in.createLongArray(); + bucketCount = bucketStart.length; + } + + /** {@inheritDoc} */ + public void writeToParcel(Parcel out, int flags) { + out.writeLong(bucketDuration); + writeLongArray(out, bucketStart, bucketCount); + writeLongArray(out, rx, bucketCount); + writeLongArray(out, tx, bucketCount); + } + + public NetworkStatsHistory(DataInputStream in) throws IOException { + final int version = in.readInt(); + switch (version) { + case VERSION_CURRENT: { + bucketDuration = in.readLong(); + bucketStart = readLongArray(in); + rx = readLongArray(in); + tx = readLongArray(in); + bucketCount = bucketStart.length; + break; + } + default: { + throw new ProtocolException("unexpected version: " + version); + } + } + } + + public void writeToStream(DataOutputStream out) throws IOException { + out.writeInt(VERSION_CURRENT); + out.writeLong(bucketDuration); + writeLongArray(out, bucketStart, bucketCount); + writeLongArray(out, rx, bucketCount); + writeLongArray(out, tx, bucketCount); + } + + /** {@inheritDoc} */ + public int describeContents() { + return 0; + } + + /** + * Record that data traffic occurred in the given time range. Will + * distribute across internal buckets, creating new buckets as needed. + */ + public void recordData(long start, long end, long rx, long tx) { + // create any buckets needed by this range + ensureBuckets(start, end); + + // distribute data usage into buckets + final long duration = end - start; + for (int i = bucketCount - 1; i >= 0; i--) { + final long curStart = bucketStart[i]; + final long curEnd = curStart + bucketDuration; + + // bucket is older than record; we're finished + if (curEnd < start) break; + // bucket is newer than record; keep looking + if (curStart > end) continue; + + final long overlap = Math.min(curEnd, end) - Math.max(curStart, start); + if (overlap > 0) { + this.rx[i] += rx * overlap / duration; + this.tx[i] += tx * overlap / duration; + } + } + } + + /** + * Record an entire {@link NetworkStatsHistory} into this history. Usually + * for combining together stats for external reporting. + */ + public void recordEntireHistory(NetworkStatsHistory input) { + for (int i = 0; i < input.bucketCount; i++) { + final long start = input.bucketStart[i]; + final long end = start + input.bucketDuration; + recordData(start, end, input.rx[i], input.tx[i]); + } + } + + /** + * Ensure that buckets exist for given time range, creating as needed. + */ + private void ensureBuckets(long start, long end) { + // normalize incoming range to bucket boundaries + start -= start % bucketDuration; + end += (bucketDuration - (end % bucketDuration)) % bucketDuration; + + for (long now = start; now < end; now += bucketDuration) { + // try finding existing bucket + final int index = Arrays.binarySearch(bucketStart, 0, bucketCount, now); + if (index < 0) { + // bucket missing, create and insert + insertBucket(~index, now); + } + } + } + + /** + * Insert new bucket at requested index and starting time. + */ + private void insertBucket(int index, long start) { + // create more buckets when needed + if (bucketCount + 1 > bucketStart.length) { + final int newLength = bucketStart.length + 10; + bucketStart = Arrays.copyOf(bucketStart, newLength); + rx = Arrays.copyOf(rx, newLength); + tx = Arrays.copyOf(tx, newLength); + } + + // create gap when inserting bucket in middle + if (index < bucketCount) { + final int dstPos = index + 1; + final int length = bucketCount - index; + + System.arraycopy(bucketStart, index, bucketStart, dstPos, length); + System.arraycopy(rx, index, rx, dstPos, length); + System.arraycopy(tx, index, tx, dstPos, length); + } + + bucketStart[index] = start; + rx[index] = 0; + tx[index] = 0; + bucketCount++; + } + + /** + * Remove buckets older than requested cutoff. + */ + public void removeBucketsBefore(long cutoff) { + int i; + for (i = 0; i < bucketCount; i++) { + final long curStart = bucketStart[i]; + final long curEnd = curStart + bucketDuration; + + // cutoff happens before or during this bucket; everything before + // this bucket should be removed. + if (curEnd > cutoff) break; + } + + if (i > 0) { + final int length = bucketStart.length; + bucketStart = Arrays.copyOfRange(bucketStart, i, length); + rx = Arrays.copyOfRange(rx, i, length); + tx = Arrays.copyOfRange(tx, i, length); + bucketCount -= i; + } + } + + /** + * Return interpolated data usage across the requested range. Interpolates + * across buckets, so values may be rounded slightly. + */ + public long[] getTotalData(long start, long end, long[] outTotal) { + long rx = 0; + long tx = 0; + + for (int i = bucketCount - 1; i >= 0; i--) { + final long curStart = bucketStart[i]; + final long curEnd = curStart + bucketDuration; + + // bucket is older than record; we're finished + if (curEnd < start) break; + // bucket is newer than record; keep looking + if (curStart > end) continue; + + final long overlap = Math.min(curEnd, end) - Math.max(curStart, start); + if (overlap > 0) { + rx += this.rx[i] * overlap / bucketDuration; + tx += this.tx[i] * overlap / bucketDuration; + } + } + + if (outTotal == null || outTotal.length != 2) { + outTotal = new long[2]; + } + outTotal[0] = rx; + outTotal[1] = tx; + return outTotal; + } + + /** + * @deprecated only for temporary testing + */ + @Deprecated + public void generateRandom(long start, long end, long rx, long tx) { + ensureBuckets(start, end); + + final Random r = new Random(); + while (rx > 1024 && tx > 1024) { + final long curStart = randomLong(r, start, end); + final long curEnd = randomLong(r, curStart, end); + final long curRx = randomLong(r, 0, rx); + final long curTx = randomLong(r, 0, tx); + + recordData(curStart, curEnd, curRx, curTx); + + rx -= curRx; + tx -= curTx; + } + } + + private static long randomLong(Random r, long start, long end) { + return (long) (start + (r.nextFloat() * (end - start))); + } + + public void dump(String prefix, PrintWriter pw) { + pw.print(prefix); + pw.print("NetworkStatsHistory: bucketDuration="); pw.println(bucketDuration); + for (int i = 0; i < bucketCount; i++) { + pw.print(prefix); + pw.print(" bucketStart="); pw.print(bucketStart[i]); + pw.print(" rx="); pw.print(rx[i]); + pw.print(" tx="); pw.println(tx[i]); + } + } + + @Override + public String toString() { + final CharArrayWriter writer = new CharArrayWriter(); + dump("", new PrintWriter(writer)); + return writer.toString(); + } + + public static final Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() { + public NetworkStatsHistory createFromParcel(Parcel in) { + return new NetworkStatsHistory(in); + } + + public NetworkStatsHistory[] newArray(int size) { + return new NetworkStatsHistory[size]; + } + }; + + private static long[] readLongArray(DataInputStream in) throws IOException { + final int size = in.readInt(); + final long[] values = new long[size]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readLong(); + } + return values; + } + + private static void writeLongArray(DataOutputStream out, long[] values, int size) throws IOException { + if (size > values.length) { + throw new IllegalArgumentException("size larger than length"); + } + out.writeInt(size); + for (int i = 0; i < size; i++) { + out.writeLong(values[i]); + } + } + + private static long[] readLongArray(Parcel in) { + final int size = in.readInt(); + final long[] values = new long[size]; + for (int i = 0; i < values.length; i++) { + values[i] = in.readLong(); + } + return values; + } + + private static void writeLongArray(Parcel out, long[] values, int size) { + if (size > values.length) { + throw new IllegalArgumentException("size larger than length"); + } + out.writeInt(size); + for (int i = 0; i < size; i++) { + out.writeLong(values[i]); + } + } + +} diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java index f8f8a29..8e50cd5 100644 --- a/core/java/android/net/SSLCertificateSocketFactory.java +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -17,30 +17,24 @@ 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; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; 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; @@ -93,6 +87,8 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { private SSLSocketFactory mInsecureFactory = null; private SSLSocketFactory mSecureFactory = null; + private TrustManager[] mTrustManagers = null; + private KeyManager[] mKeyManagers = null; private final int mHandshakeTimeoutMillis; private final SSLClientSessionCache mSessionCache; @@ -128,7 +124,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 +140,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 +153,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)); } @@ -205,10 +200,11 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { } } - private SSLSocketFactory makeSocketFactory(TrustManager[] trustManagers) { + private SSLSocketFactory makeSocketFactory( + KeyManager[] keyManagers, TrustManager[] trustManagers) { try { OpenSSLContextImpl sslContext = new OpenSSLContextImpl(); - sslContext.engineInit(null, trustManagers, null); + sslContext.engineInit(keyManagers, trustManagers, null); sslContext.engineGetClientSessionContext().setPersistentCache(mSessionCache); return sslContext.engineGetSocketFactory(); } catch (KeyManagementException e) { @@ -231,18 +227,44 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { } else { Log.w(TAG, "Bypassing SSL security checks at caller's request"); } - mInsecureFactory = makeSocketFactory(INSECURE_TRUST_MANAGER); + mInsecureFactory = makeSocketFactory(mKeyManagers, INSECURE_TRUST_MANAGER); } return mInsecureFactory; } else { if (mSecureFactory == null) { - mSecureFactory = makeSocketFactory(null); + mSecureFactory = makeSocketFactory(mKeyManagers, mTrustManagers); } return mSecureFactory; } } /** + * Sets the {@link TrustManager}s to be used for connections made by this factory. + * @hide + */ + public void setTrustManagers(TrustManager[] trustManager) { + mTrustManagers = trustManager; + + // Clear out all cached secure factories since configurations have changed. + mSecureFactory = null; + // Note - insecure factories only ever use the INSECURE_TRUST_MANAGER so they need not + // be cleared out here. + } + + /** + * Sets the {@link KeyManager}s to be used for connections made by this factory. + * @hide + */ + public void setKeyManagers(KeyManager[] keyManagers) { + mKeyManagers = keyManagers; + + // Clear out any existing cached factories since configurations have changed. + mSecureFactory = null; + mInsecureFactory = null; + } + + + /** * {@inheritDoc} * * <p>This method verifies the peer's certificate hostname after connecting diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java index 3e21e2d..316440f 100644 --- a/core/java/android/net/SntpClient.java +++ b/core/java/android/net/SntpClient.java @@ -17,7 +17,6 @@ package android.net; import android.os.SystemClock; -import android.util.Config; import android.util.Log; import java.io.IOException; @@ -112,8 +111,8 @@ public class SntpClient // = (transit + skew - transit + skew)/2 // = (2 * skew)/2 = skew long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2; - // if (Config.LOGD) Log.d(TAG, "round trip: " + roundTripTime + " ms"); - // if (Config.LOGD) Log.d(TAG, "clock offset: " + clockOffset + " ms"); + // if (false) Log.d(TAG, "round trip: " + roundTripTime + " ms"); + // if (false) Log.d(TAG, "clock offset: " + clockOffset + " ms"); // save our results - use the times on this side of the network latency // (response rather than request time) @@ -121,7 +120,7 @@ public class SntpClient mNtpTimeReference = responseTicks; mRoundTripTime = roundTripTime; } catch (Exception e) { - if (Config.LOGD) Log.d(TAG, "request time failed: " + e); + if (false) Log.d(TAG, "request time failed: " + e); return false; } finally { if (socket != null) { diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java index eca06c5..8a688d5 100644 --- a/core/java/android/net/TrafficStats.java +++ b/core/java/android/net/TrafficStats.java @@ -16,11 +16,16 @@ package android.net; -import android.util.Log; +import android.content.Context; +import android.os.IBinder; +import android.os.INetworkManagementService; +import android.os.RemoteException; +import android.os.ServiceManager; -import java.io.File; -import java.io.RandomAccessFile; -import java.io.IOException; +import dalvik.system.BlockGuard; + +import java.net.Socket; +import java.net.SocketException; /** * Class that provides network traffic statistics. These statistics include @@ -36,6 +41,149 @@ public class TrafficStats { */ public final static int UNSUPPORTED = -1; + // TODO: find better home for these template constants + + /** + * Template to combine all {@link ConnectivityManager#TYPE_MOBILE} style + * networks together. Only uses statistics for currently active IMSI. + * + * @hide + */ + public static final int TEMPLATE_MOBILE_ALL = 1; + + /** + * Template to combine all {@link ConnectivityManager#TYPE_MOBILE} style + * networks together that roughly meet a "3G" definition, or lower. Only + * uses statistics for currently active IMSI. + * + * @hide + */ + public static final int TEMPLATE_MOBILE_3G_LOWER = 2; + + /** + * Template to combine all {@link ConnectivityManager#TYPE_MOBILE} style + * networks together that meet a "4G" definition. Only uses statistics for + * currently active IMSI. + * + * @hide + */ + public static final int TEMPLATE_MOBILE_4G = 3; + + /** + * Template to combine all {@link ConnectivityManager#TYPE_WIFI} style + * networks together. + * + * @hide + */ + public static final int TEMPLATE_WIFI = 4; + + /** + * Snapshot of {@link NetworkStats} when the currently active profiling + * session started, or {@code null} if no session active. + * + * @see #startDataProfiling(Context) + * @see #stopDataProfiling(Context) + */ + private static NetworkStats sActiveProfilingStart; + + private static Object sProfilingLock = new Object(); + + /** + * Set active tag to use when accounting {@link Socket} traffic originating + * from the current thread. Only one active tag per thread is supported. + * <p> + * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + */ + public static void setThreadStatsTag(String tag) { + BlockGuard.setThreadSocketStatsTag(tag); + } + + public static void clearThreadStatsTag() { + BlockGuard.setThreadSocketStatsTag(null); + } + + /** + * Set specific UID to use when accounting {@link Socket} traffic + * originating from the current thread. Designed for use when performing an + * operation on behalf of another application. + * <p> + * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + * <p> + * To take effect, caller must hold + * {@link android.Manifest.permission#UPDATE_DEVICE_STATS} permission. + * + * {@hide} + */ + public static void setThreadStatsUid(int uid) { + BlockGuard.setThreadSocketStatsUid(uid); + } + + /** {@hide} */ + public static void clearThreadStatsUid() { + BlockGuard.setThreadSocketStatsUid(-1); + } + + /** + * Tag the given {@link Socket} with any statistics parameters active for + * the current thread. Subsequent calls always replace any existing + * parameters. When finished, call {@link #untagSocket(Socket)} to remove + * statistics parameters. + * + * @see #setThreadStatsTag(String) + * @see #setThreadStatsUid(int) + */ + public static void tagSocket(Socket socket) throws SocketException { + BlockGuard.tagSocketFd(socket.getFileDescriptor$()); + } + + /** + * Remove any statistics parameters from the given {@link Socket}. + */ + public static void untagSocket(Socket socket) throws SocketException { + BlockGuard.untagSocketFd(socket.getFileDescriptor$()); + } + + /** + * Start profiling data usage for current UID. Only one profiling session + * can be active at a time. + * + * @hide + */ + public static void startDataProfiling(Context context) { + synchronized (sProfilingLock) { + if (sActiveProfilingStart != null) { + throw new IllegalStateException("already profiling data"); + } + + // take snapshot in time; we calculate delta later + sActiveProfilingStart = getNetworkStatsForUid(context); + } + } + + /** + * Stop profiling data usage for current UID. + * + * @return Detailed {@link NetworkStats} of data that occurred since last + * {@link #startDataProfiling(Context)} call. + * @hide + */ + public static NetworkStats stopDataProfiling(Context context) { + synchronized (sProfilingLock) { + if (sActiveProfilingStart == null) { + throw new IllegalStateException("not profiling data"); + } + + // subtract starting values and return delta + final NetworkStats profilingStop = getNetworkStatsForUid(context); + final NetworkStats profilingDelta = profilingStop.subtractClamped( + sActiveProfilingStart); + sActiveProfilingStart = null; + return profilingDelta; + } + } + /** * Get the total number of packets transmitted through the mobile interface. * @@ -294,4 +442,21 @@ public class TrafficStats { * {@link #UNSUPPORTED} will be returned. */ public static native long getUidUdpRxPackets(int uid); + + /** + * Return detailed {@link NetworkStats} for the current UID. Requires no + * special permission. + */ + private static NetworkStats getNetworkStatsForUid(Context context) { + final IBinder binder = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); + final INetworkManagementService service = INetworkManagementService.Stub.asInterface( + binder); + + final int uid = android.os.Process.myUid(); + try { + return service.getNetworkStatsUidDetail(uid); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } } 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/HttpResponseCache.java b/core/java/android/net/http/HttpResponseCache.java new file mode 100644 index 0000000..b5d64e4 --- /dev/null +++ b/core/java/android/net/http/HttpResponseCache.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.http; + +import android.content.Context; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.net.CacheRequest; +import java.net.CacheResponse; +import java.net.HttpURLConnection; +import java.net.ResponseCache; +import java.net.URI; +import java.net.URLConnection; +import java.util.List; +import java.util.Map; +import javax.net.ssl.HttpsURLConnection; +import libcore.io.DiskLruCache; +import libcore.io.IoUtils; +import org.apache.http.impl.client.DefaultHttpClient; + +/** + * Caches HTTP and HTTPS responses to the filesystem so they may be reused, + * saving time and bandwidth. This class supports {@link HttpURLConnection} and + * {@link HttpsURLConnection}; there is no platform-provided cache for {@link + * DefaultHttpClient} or {@link AndroidHttpClient}. + * + * <h3>Installing an HTTP response cache</h3> + * Enable caching of all of your application's HTTP requests by installing the + * cache at application startup. For example, this code installs a 10 MiB cache + * in the {@link Context#getCacheDir() application-specific cache directory} of + * the filesystem}: <pre> {@code + * protected void onCreate(Bundle savedInstanceState) { + * ... + * + * try { + * File httpCacheDir = new File(context.getCacheDir(), "http"); + * long httpCacheSize = 10 * 1024 * 1024; // 10 MiB + * HttpResponseCache.install(httpCacheDir, httpCacheSize); + * } catch (IOException e) { + * Log.i(TAG, "HTTP response cache installation failed:" + e); + * } + * } + * + * protected void onStop() { + * ... + * + * HttpResponseCache cache = HttpResponseCache.getInstalled(); + * if (cache != null) { + * cache.flush(); + * } + * }}</pre> + * This cache will evict entries as necessary to keep its size from exceeding + * 10 MiB. The best cache size is application specific and depends on the size + * and frequency of the files being downloaded. Increasing the limit may improve + * the hit rate, but it may also just waste filesystem space! + * + * <p>For some applications it may be preferable to create the cache in the + * external storage directory. Although it often has more free space, external + * storage is optional and—even if available—can disappear during + * use. Retrieve the external cache directory using {@link Context#getExternalCacheDir()}. If this method + * returns null, your application should fall back to either not caching or + * caching on non-external storage. If the external storage is removed during + * use, the cache hit rate will drop to zero and ongoing cache reads will fail. + * + * <p>Flushing the cache forces its data to the filesystem. This ensures that + * all responses written to the cache will be readable the next time the + * activity starts. + * + * <h3>Cache Optimization</h3> + * To measure cache effectiveness, this class tracks three statistics: + * <ul> + * <li><strong>{@link #getRequestCount() Request Count:}</strong> the number + * of HTTP requests issued since this cache was created. + * <li><strong>{@link #getNetworkCount() Network Count:}</strong> the + * number of those requests that required network use. + * <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of + * those requests whose responses were served by the cache. + * </ul> + * Sometimes a request will result in a conditional cache hit. If the cache + * contains a stale copy of the response, the client will issue a conditional + * {@code GET}. The server will then send either the updated response if it has + * changed, or a short 'not modified' response if the client's copy is still + * valid. Such responses increment both the network count and hit count. + * + * <p>The best way to improve the cache hit rate is by configuring the web + * server to return cacheable responses. Although this client honors all <a + * href="http://www.ietf.org/rfc/rfc2616.txt">HTTP/1.1 (RFC 2068)</a> cache + * headers, it doesn't cache partial responses. + * + * <h3>Force a Network Response</h3> + * In some situations, such as after a user clicks a 'refresh' button, it may be + * necessary to skip the cache, and fetch data directly from the server. To force + * a full refresh, add the {@code no-cache} directive: <pre> {@code + * connection.addRequestProperty("Cache-Control", "no-cache"); + * }</pre> + * If it is only necessary to force a cached response to be validated by the + * server, use the more efficient {@code max-age=0} instead: <pre> {@code + * connection.addRequestProperty("Cache-Control", "max-age=0"); + * }</pre> + * + * <h3>Force a Cache Response</h3> + * Sometimes you'll want to show resources if they are available immediately, + * but not otherwise. This can be used so your application can show + * <i>something</i> while waiting for the latest data to be downloaded. To + * restrict a request to locally-cached resources, add the {@code + * only-if-cached} directive: <pre> {@code + * try { + * connection.addRequestProperty("Cache-Control", "only-if-cached"); + * InputStream cached = connection.getInputStream(); + * // the resource was cached! show it + * } catch (FileNotFoundException e) { + * // the resource was not cached + * } + * }</pre> + * This technique works even better in situations where a stale response is + * better than no response. To permit stale cached responses, use the {@code + * max-stale} directive with the maximum staleness in seconds: <pre> {@code + * int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale + * connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale); + * }</pre> + */ +public final class HttpResponseCache extends ResponseCache implements Closeable { + + private final libcore.net.http.HttpResponseCache delegate; + + private HttpResponseCache(File directory, long maxSize) throws IOException { + this.delegate = new libcore.net.http.HttpResponseCache(directory, maxSize); + } + + /** + * Returns the currently-installed {@code HttpResponseCache}, or null if + * there is no cache installed or it is not a {@code HttpResponseCache}. + */ + public static HttpResponseCache getInstalled() { + ResponseCache installed = ResponseCache.getDefault(); + return installed instanceof HttpResponseCache ? (HttpResponseCache) installed : null; + } + + /** + * Creates a new HTTP response cache and {@link ResponseCache#setDefault + * sets it} as the system default cache. + * + * @param directory the directory to hold cache data. + * @param maxSize the maximum size of the cache in bytes. + * @return the newly-installed cache + * @throws IOException if {@code directory} cannot be used for this cache. + * Most applications should respond to this exception by logging a + * warning. + */ + public static HttpResponseCache install(File directory, long maxSize) throws IOException { + HttpResponseCache installed = getInstalled(); + if (installed != null) { + // don't close and reopen if an equivalent cache is already installed + DiskLruCache installedCache = installed.delegate.getCache(); + if (installedCache.getDirectory().equals(directory) + && installedCache.maxSize() == maxSize + && !installedCache.isClosed()) { + return installed; + } else { + IoUtils.closeQuietly(installed); + } + } + + HttpResponseCache result = new HttpResponseCache(directory, maxSize); + ResponseCache.setDefault(result); + return result; + } + + @Override public CacheResponse get(URI uri, String requestMethod, + Map<String, List<String>> requestHeaders) throws IOException { + return delegate.get(uri, requestMethod, requestHeaders); + } + + @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException { + return delegate.put(uri, urlConnection); + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the {@link #maxSize} if a background + * deletion is pending. + */ + public long size() { + return delegate.getCache().size(); + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public long maxSize() { + return delegate.getCache().maxSize(); + } + + /** + * Force buffered operations to the filesystem. This ensures that responses + * written to the cache will be available the next time the cache is opened, + * even if this process is killed. + */ + public void flush() { + try { + delegate.getCache().flush(); // TODO: fix flush() to not throw? + } catch (IOException ignored) { + } + } + + /** + * Returns the number of HTTP requests that required the network to either + * supply a response or validate a locally cached response. + */ + public int getNetworkCount() { + return delegate.getNetworkCount(); + } + + /** + * Returns the number of HTTP requests whose response was provided by the + * cache. This may include conditional {@code GET} requests that were + * validated over the network. + */ + public int getHitCount() { + return delegate.getHitCount(); + } + + /** + * Returns the total number of HTTP requests that were made. This includes + * both client requests and requests that were made on the client's behalf + * to handle a redirects and retries. + */ + public int getRequestCount() { + return delegate.getRequestCount(); + } + + /** + * Uninstalls the cache and releases any active resources. Stored contents + * will remain on the filesystem. + */ + @Override public void close() throws IOException { + if (ResponseCache.getDefault() == this) { + ResponseCache.setDefault(null); + } + delegate.getCache().close(); + } + + /** + * Uninstalls the cache and deletes all of its stored contents. + */ + public void delete() throws IOException { + if (ResponseCache.getDefault() == this) { + ResponseCache.setDefault(null); + } + delegate.getCache().delete(); + } +} 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/nfc/ErrorCodes.java b/core/java/android/nfc/ErrorCodes.java index 69329df..3adcdc3 100644 --- a/core/java/android/nfc/ErrorCodes.java +++ b/core/java/android/nfc/ErrorCodes.java @@ -57,6 +57,7 @@ public class ErrorCodes { case ERROR_SE_ALREADY_SELECTED: return "SE_ALREADY_SELECTED"; case ERROR_SE_CONNECTED: return "SE_CONNECTED"; case ERROR_NO_SE_CONNECTED: return "NO_SE_CONNECTED"; + case ERROR_NOT_SUPPORTED: return "NOT_SUPPORTED"; default: return "UNKNOWN ERROR"; } } @@ -105,4 +106,6 @@ public class ErrorCodes { public static final int ERROR_NO_SE_CONNECTED = -20; -}
\ No newline at end of file + public static final int ERROR_NOT_SUPPORTED = -21; + +} diff --git a/core/java/android/nfc/INdefPushCallback.aidl b/core/java/android/nfc/INdefPushCallback.aidl new file mode 100644 index 0000000..80ba2ed --- /dev/null +++ b/core/java/android/nfc/INdefPushCallback.aidl @@ -0,0 +1,28 @@ +/* + * 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.nfc; + +import android.nfc.NdefMessage; + +/** + * @hide + */ +interface INdefPushCallback +{ + NdefMessage onConnect(); + void onMessagePushed(); +} diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl index 870127c..d11fea0 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -25,6 +25,7 @@ import android.nfc.TechListParcel; import android.nfc.ILlcpSocket; import android.nfc.ILlcpServiceSocket; import android.nfc.ILlcpConnectionlessSocket; +import android.nfc.INdefPushCallback; import android.nfc.INfcTag; import android.nfc.IP2pTarget; import android.nfc.IP2pInitiator; @@ -51,6 +52,7 @@ interface INfcAdapter in IntentFilter[] filters, in TechListParcel techLists); void disableForegroundDispatch(in ComponentName activity); void enableForegroundNdefPush(in ComponentName activity, in NdefMessage msg); + void enableForegroundNdefPushWithCallback(in ComponentName activity, in INdefPushCallback callback); void disableForegroundNdefPush(in ComponentName activity); // Non-public methods diff --git a/core/java/android/nfc/INfcTag.aidl b/core/java/android/nfc/INfcTag.aidl index 57dc38c..b66035f 100644 --- a/core/java/android/nfc/INfcTag.aidl +++ b/core/java/android/nfc/INfcTag.aidl @@ -17,6 +17,7 @@ package android.nfc; import android.nfc.NdefMessage; +import android.nfc.Tag; import android.nfc.TransceiveResult; /** @@ -40,7 +41,9 @@ interface INfcTag int ndefMakeReadOnly(int nativeHandle); boolean ndefIsWritable(int nativeHandle); int formatNdef(int nativeHandle, in byte[] key); + Tag rediscover(int nativehandle); void setIsoDepTimeout(int timeout); - void resetIsoDepTimeout(); + void setFelicaTimeout(int timeout); + void resetTimeouts(); } diff --git a/core/java/android/nfc/NdefRecord.java b/core/java/android/nfc/NdefRecord.java index 746d3df..3fd26dd 100644 --- a/core/java/android/nfc/NdefRecord.java +++ b/core/java/android/nfc/NdefRecord.java @@ -16,10 +16,13 @@ package android.nfc; +import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import java.lang.UnsupportedOperationException; +import java.nio.charset.Charsets; +import java.util.Arrays; /** * Represents a logical (unchunked) NDEF (NFC Data Exchange Format) record. @@ -142,6 +145,50 @@ public final class NdefRecord implements Parcelable { private static final byte FLAG_SR = (byte) 0x10; private static final byte FLAG_IL = (byte) 0x08; + /** + * NFC Forum "URI Record Type Definition" + * + * This is a mapping of "URI Identifier Codes" to URI string prefixes, + * per section 3.2.2 of the NFC Forum URI Record Type Definition document. + */ + private static final String[] URI_PREFIX_MAP = new String[] { + "", // 0x00 + "http://www.", // 0x01 + "https://www.", // 0x02 + "http://", // 0x03 + "https://", // 0x04 + "tel:", // 0x05 + "mailto:", // 0x06 + "ftp://anonymous:anonymous@", // 0x07 + "ftp://ftp.", // 0x08 + "ftps://", // 0x09 + "sftp://", // 0x0A + "smb://", // 0x0B + "nfs://", // 0x0C + "ftp://", // 0x0D + "dav://", // 0x0E + "news:", // 0x0F + "telnet://", // 0x10 + "imap:", // 0x11 + "rtsp://", // 0x12 + "urn:", // 0x13 + "pop:", // 0x14 + "sip:", // 0x15 + "sips:", // 0x16 + "tftp:", // 0x17 + "btspp://", // 0x18 + "btl2cap://", // 0x19 + "btgoep://", // 0x1A + "tcpobex://", // 0x1B + "irdaobex://", // 0x1C + "file://", // 0x1D + "urn:epc:id:", // 0x1E + "urn:epc:tag:", // 0x1F + "urn:epc:pat:", // 0x20 + "urn:epc:raw:", // 0x21 + "urn:epc:", // 0x22 + }; + private final byte mFlags; private final short mTnf; private final byte[] mType; @@ -163,6 +210,18 @@ public final class NdefRecord implements Parcelable { * must not be null */ public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) { + /* New NDEF records created by applications will have FLAG_MB|FLAG_ME + * set by default; when multiple records are stored in a + * {@link NdefMessage}, these flags will be corrected when the {@link NdefMessage} + * is serialized to bytes. + */ + this(tnf, type, id, payload, (byte)(FLAG_MB|FLAG_ME)); + } + + /** + * @hide + */ + /*package*/ NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload, byte flags) { /* check arguments */ if ((type == null) || (id == null) || (payload == null)) { throw new IllegalArgumentException("Illegal null argument"); @@ -172,9 +231,6 @@ public final class NdefRecord implements Parcelable { throw new IllegalArgumentException("TNF out of range " + tnf); } - /* generate flag */ - byte flags = FLAG_MB | FLAG_ME; - /* Determine if it is a short record */ if(payload.length < 0xFF) { flags |= FLAG_SR; @@ -247,6 +303,50 @@ public final class NdefRecord implements Parcelable { } /** + * Helper to return the NdefRecord as a URI. + * TODO: Consider making a member method instead of static + * TODO: Consider more validation that this is a URI record + * TODO: Make a public API + * @hide + */ + public static Uri parseWellKnownUriRecord(NdefRecord record) throws FormatException { + byte[] payload = record.getPayload(); + if (payload.length < 2) { + throw new FormatException("Payload is not a valid URI (missing prefix)"); + } + + /* + * payload[0] contains the URI Identifier Code, per the + * NFC Forum "URI Record Type Definition" section 3.2.2. + * + * payload[1]...payload[payload.length - 1] contains the rest of + * the URI. + */ + int prefixIndex = (payload[0] & 0xff); + if (prefixIndex < 0 || prefixIndex >= URI_PREFIX_MAP.length) { + throw new FormatException("Payload is not a valid URI (invalid prefix)"); + } + String prefix = URI_PREFIX_MAP[prefixIndex]; + byte[] fullUri = concat(prefix.getBytes(Charsets.UTF_8), + Arrays.copyOfRange(payload, 1, payload.length)); + return Uri.parse(new String(fullUri, Charsets.UTF_8)); + } + + private static byte[] concat(byte[]... arrays) { + int length = 0; + for (byte[] array : arrays) { + length += array.length; + } + byte[] result = new byte[length]; + int pos = 0; + for (byte[] array : arrays) { + System.arraycopy(array, 0, result, pos, array.length); + pos += array.length; + } + return result; + } + + /** * Returns this entire NDEF Record as a byte array. */ public byte[] toByteArray() { @@ -258,6 +358,7 @@ public final class NdefRecord implements Parcelable { } public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mFlags); dest.writeInt(mTnf); dest.writeInt(mType.length); dest.writeByteArray(mType); @@ -270,6 +371,7 @@ public final class NdefRecord implements Parcelable { public static final Parcelable.Creator<NdefRecord> CREATOR = new Parcelable.Creator<NdefRecord>() { public NdefRecord createFromParcel(Parcel in) { + byte flags = (byte)in.readInt(); short tnf = (short)in.readInt(); int typeLength = in.readInt(); byte[] type = new byte[typeLength]; @@ -281,7 +383,7 @@ public final class NdefRecord implements Parcelable { byte[] payload = new byte[payloadLength]; in.readByteArray(payload); - return new NdefRecord(tnf, type, id, payload); + return new NdefRecord(tnf, type, id, payload, flags); } public NdefRecord[] newArray(int size) { return new NdefRecord[size]; @@ -290,4 +392,4 @@ public final class NdefRecord implements Parcelable { private native int parseNdefRecord(byte[] data); private native byte[] generate(short flags, short tnf, byte[] type, byte[] id, byte[] data); -}
\ No newline at end of file +} diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 4689804..738e75f 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -124,7 +124,7 @@ public final class NfcAdapter { * Intent to start an activity when a tag is discovered. * * <p>This intent will not be started when a tag is discovered if any activities respond to - * {@link #ACTION_NDEF_DISCOVERED} or {@link #ACTION_TECH_DISCOVERED} for the current tag. + * {@link #ACTION_NDEF_DISCOVERED} or {@link #ACTION_TECH_DISCOVERED} for the current tag. */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_TAG_DISCOVERED = "android.nfc.action.TAG_DISCOVERED"; @@ -235,6 +235,37 @@ public final class NfcAdapter { */ private static final int DISCOVERY_MODE_CARD_EMULATION = 2; + /** + * Callback passed into {@link #enableForegroundNdefPush(Activity,NdefPushCallback)}. This + */ + public interface NdefPushCallback { + /** + * Called when a P2P connection is created. + */ + NdefMessage createMessage(); + /** + * Called when the message is pushed. + */ + void onMessagePushed(); + } + + private static class NdefPushCallbackWrapper extends INdefPushCallback.Stub { + private NdefPushCallback mCallback; + + public NdefPushCallbackWrapper(NdefPushCallback callback) { + mCallback = callback; + } + + @Override + public NdefMessage onConnect() { + return mCallback.createMessage(); + } + + @Override + public void onMessagePushed() { + mCallback.onMessagePushed(); + } + } // Guarded by NfcAdapter.class private static boolean sIsInitialized = false; @@ -575,6 +606,44 @@ public final class NfcAdapter { } /** + * Enable NDEF message push over P2P while this Activity is in the foreground. + * + * <p>For this to function properly the other NFC device being scanned must + * support the "com.android.npp" NDEF push protocol. Support for this + * protocol is currently optional for Android NFC devices. + * + * <p>This method must be called from the main thread. + * + * <p class="note"><em>NOTE:</em> While foreground NDEF push is active standard tag dispatch is disabled. + * Only the foreground activity may receive tag discovered dispatches via + * {@link #enableForegroundDispatch}. + * + * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param activity the foreground Activity + * @param callback is called on when the P2P connection is established + * @throws IllegalStateException if the Activity is not currently in the foreground + * @throws OperationNotSupportedException if this Android device does not support NDEF push + */ + public void enableForegroundNdefPush(Activity activity, NdefPushCallback callback) { + if (activity == null || callback == null) { + throw new NullPointerException(); + } + if (!activity.isResumed()) { + throw new IllegalStateException("Foregorund NDEF push can only be enabled " + + "when your activity is resumed"); + } + try { + ActivityThread.currentActivityThread().registerOnActivityPausedListener(activity, + mForegroundNdefPushListener); + sService.enableForegroundNdefPushWithCallback(activity.getComponentName(), + new NdefPushCallbackWrapper(callback)); + } catch (RemoteException e) { + attemptDeadServiceRecovery(e); + } + } + + /** * Disable NDEF message push over P2P. * * <p>After calling {@link #enableForegroundNdefPush}, an activity diff --git a/core/java/android/nfc/Tag.java b/core/java/android/nfc/Tag.java index b676975..54583d6 100644 --- a/core/java/android/nfc/Tag.java +++ b/core/java/android/nfc/Tag.java @@ -30,7 +30,9 @@ import android.nfc.tech.TagTechnology; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; +import java.io.IOException; import java.util.Arrays; /** @@ -233,6 +235,50 @@ public final class Tag implements Parcelable { return mTechStringList; } + /** + * Rediscover the technologies available on this tag. + * <p> + * The technologies that are available on a tag may change due to + * operations being performed on a tag. For example, formatting a + * tag as NDEF adds the {@link Ndef} technology. The {@link rediscover} + * method reenumerates the available technologies on the tag + * and returns a new {@link Tag} object containing these technologies. + * <p> + * You may not be connected to any of this {@link Tag}'s technologies + * when calling this method. + * This method guarantees that you will be returned the same Tag + * if it is still in the field. + * <p>May cause RF activity and may block. Must not be called + * from the main application thread. A blocked call will be canceled with + * {@link IOException} by calling {@link #close} from another thread. + * <p>Does not remove power from the RF field, so a tag having a random + * ID should not change its ID. + * @return the rediscovered tag object. + * @throws IOException if the tag cannot be rediscovered + * @hide + */ + // TODO See if we need TagLostException + // TODO Unhide for ICS + // TODO Update documentation to make sure it matches with the final + // implementation. + public Tag rediscover() throws IOException { + if (getConnectedTechnology() != -1) { + throw new IllegalStateException("Close connection to the technology first!"); + } + + try { + Tag newTag = mTagService.rediscover(getServiceHandle()); + if (newTag != null) { + return newTag; + } else { + throw new IOException("Failed to rediscover tag"); + } + } catch (RemoteException e) { + throw new IOException("NFC service dead"); + } + } + + /** @hide */ public boolean hasTech(int techType) { for (int tech : mTechList) { diff --git a/core/java/android/nfc/tech/BasicTagTechnology.java b/core/java/android/nfc/tech/BasicTagTechnology.java index 7ec807a..bcb7199 100644 --- a/core/java/android/nfc/tech/BasicTagTechnology.java +++ b/core/java/android/nfc/tech/BasicTagTechnology.java @@ -77,6 +77,10 @@ import java.io.IOException; // Store this in the tag object mTag.setConnectedTechnology(mSelectedTechnology); mIsConnected = true; + } else if (errorCode == ErrorCodes.ERROR_NOT_SUPPORTED) { + throw new UnsupportedOperationException("Connecting to " + + "this technology is not supported by the NFC " + + "adapter."); } else { throw new IOException(); } @@ -115,6 +119,7 @@ import java.io.IOException; /* Note that we don't want to physically disconnect the tag, * but just reconnect to it to reset its state */ + mTag.getTagService().resetTimeouts(); mTag.getTagService().reconnect(mTag.getServiceHandle()); } catch (RemoteException e) { Log.e(TAG, "NFC service dead", e); diff --git a/core/java/android/nfc/tech/IsoDep.java b/core/java/android/nfc/tech/IsoDep.java index 9c3074b..38b2bbd 100644 --- a/core/java/android/nfc/tech/IsoDep.java +++ b/core/java/android/nfc/tech/IsoDep.java @@ -96,16 +96,6 @@ public final class IsoDep extends BasicTagTechnology { } } - @Override - public void close() throws IOException { - try { - mTag.getTagService().resetIsoDepTimeout(); - } catch (RemoteException e) { - Log.e(TAG, "NFC service dead", e); - } - super.close(); - } - /** * Return the ISO-DEP historical bytes for {@link NfcA} tags. * <p>Does not cause any RF activity and does not block. diff --git a/core/java/android/nfc/tech/MifareUltralight.java b/core/java/android/nfc/tech/MifareUltralight.java index 7a6e79c..6c2754b 100644 --- a/core/java/android/nfc/tech/MifareUltralight.java +++ b/core/java/android/nfc/tech/MifareUltralight.java @@ -18,6 +18,7 @@ package android.nfc.tech; import android.nfc.Tag; import android.nfc.TagLostException; +import android.os.Bundle; import android.os.RemoteException; import java.io.IOException; @@ -69,6 +70,9 @@ public final class MifareUltralight extends BasicTagTechnology { private static final int NXP_MANUFACTURER_ID = 0x04; private static final int MAX_PAGE_COUNT = 256; + /** @hide */ + public static final String EXTRA_IS_UL_C = "isulc"; + private int mType; /** @@ -101,10 +105,12 @@ public final class MifareUltralight extends BasicTagTechnology { mType = TYPE_UNKNOWN; if (a.getSak() == 0x00 && tag.getId()[0] == NXP_MANUFACTURER_ID) { - // could be UL or UL-C - //TODO: stack should use NXP AN1303 procedure to make a best guess - // attempt at classifying Ultralight vs Ultralight C. - mType = TYPE_ULTRALIGHT; + Bundle extras = tag.getTechExtras(TagTechnology.MIFARE_ULTRALIGHT); + if (extras.getBoolean(EXTRA_IS_UL_C)) { + mType = TYPE_ULTRALIGHT_C; + } else { + mType = TYPE_ULTRALIGHT; + } } } diff --git a/core/java/android/nfc/tech/NfcF.java b/core/java/android/nfc/tech/NfcF.java index e0ebbe8..250c9b3 100644 --- a/core/java/android/nfc/tech/NfcF.java +++ b/core/java/android/nfc/tech/NfcF.java @@ -19,6 +19,7 @@ package android.nfc.tech; import android.nfc.Tag; import android.os.Bundle; import android.os.RemoteException; +import android.util.Log; import java.io.IOException; @@ -33,6 +34,8 @@ import java.io.IOException; * require the {@link android.Manifest.permission#NFC} permission. */ public final class NfcF extends BasicTagTechnology { + private static final String TAG = "NFC"; + /** @hide */ public static final String EXTRA_SC = "systemcode"; /** @hide */ @@ -111,4 +114,26 @@ public final class NfcF extends BasicTagTechnology { public byte[] transceive(byte[] data) throws IOException { return transceive(data, true); } + + /** + * Set the timeout of {@link #transceive} in milliseconds. + * <p>The timeout only applies to NfcF {@link #transceive}, and is + * reset to a default value when {@link #close} is called. + * <p>Setting a longer timeout may be useful when performing + * transactions that require a long processing time on the tag + * such as key generation. + * + * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param timeout timeout value in milliseconds + * @hide + */ + // TODO Unhide for ICS + public void setTimeout(int timeout) { + try { + mTag.getTagService().setFelicaTimeout(timeout); + } catch (RemoteException e) { + Log.e(TAG, "NFC service dead", e); + } + } } diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index 1803604..64bba54 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -153,7 +153,6 @@ public abstract class AsyncTask<Params, Progress, Result> { private static final int MAXIMUM_POOL_SIZE = 128; private static final int KEEP_ALIVE = 1; - private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); @@ -183,6 +182,7 @@ public abstract class AsyncTask<Params, Progress, Result> { private static final InternalHandler sHandler = new InternalHandler(); + private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; private final WorkerRunnable<Params, Result> mWorker; private final FutureTask<Result> mFuture; @@ -240,6 +240,11 @@ public abstract class AsyncTask<Params, Progress, Result> { sHandler.getLooper(); } + /** @hide */ + public static void setDefaultExecutor(Executor exec) { + sDefaultExecutor = exec; + } + /** * Creates a new asynchronous task. This constructor must be invoked on the UI thread. */ @@ -496,7 +501,7 @@ public abstract class AsyncTask<Params, Progress, Result> { * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. */ public final AsyncTask<Params, Progress, Result> execute(Params... params) { - return executeOnExecutor(THREAD_POOL_EXECUTOR, params); + return executeOnExecutor(sDefaultExecutor, params); } /** @@ -559,7 +564,7 @@ public abstract class AsyncTask<Params, Progress, Result> { * a simple Runnable object. */ public static void execute(Runnable runnable) { - THREAD_POOL_EXECUTOR.execute(runnable); + sDefaultExecutor.execute(runnable); } /** diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 1d6bc4e..e344197 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -26,6 +26,7 @@ import android.content.pm.ApplicationInfo; import android.telephony.SignalStrength; import android.util.Log; import android.util.Printer; +import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; @@ -408,15 +409,19 @@ public abstract class BatteryStats implements Parcelable { } public final static class HistoryItem implements Parcelable { + static final String TAG = "HistoryItem"; + static final boolean DEBUG = false; + public HistoryItem next; public long time; - public static final byte CMD_UPDATE = 0; - public static final byte CMD_START = 1; - public static final byte CMD_OVERFLOW = 2; + public static final byte CMD_NULL = 0; + public static final byte CMD_UPDATE = 1; + public static final byte CMD_START = 2; + public static final byte CMD_OVERFLOW = 3; - public byte cmd; + public byte cmd = CMD_NULL; public byte batteryLevel; public byte batteryStatus; @@ -427,33 +432,38 @@ public abstract class BatteryStats implements Parcelable { public char batteryVoltage; // Constants from SCREEN_BRIGHTNESS_* - public static final int STATE_BRIGHTNESS_MASK = 0x000000f; + public static final int STATE_BRIGHTNESS_MASK = 0x0000000f; public static final int STATE_BRIGHTNESS_SHIFT = 0; // Constants from SIGNAL_STRENGTH_* - public static final int STATE_SIGNAL_STRENGTH_MASK = 0x00000f0; + public static final int STATE_SIGNAL_STRENGTH_MASK = 0x000000f0; public static final int STATE_SIGNAL_STRENGTH_SHIFT = 4; // Constants from ServiceState.STATE_* - public static final int STATE_PHONE_STATE_MASK = 0x0000f00; + public static final int STATE_PHONE_STATE_MASK = 0x00000f00; public static final int STATE_PHONE_STATE_SHIFT = 8; // Constants from DATA_CONNECTION_* - public static final int STATE_DATA_CONNECTION_MASK = 0x000f000; + public static final int STATE_DATA_CONNECTION_MASK = 0x0000f000; public static final int STATE_DATA_CONNECTION_SHIFT = 12; - public static final int STATE_BATTERY_PLUGGED_FLAG = 1<<30; - public static final int STATE_SCREEN_ON_FLAG = 1<<29; + // These states always appear directly in the first int token + // of a delta change; they should be ones that change relatively + // frequently. + public static final int STATE_WAKE_LOCK_FLAG = 1<<30; + public static final int STATE_SENSOR_ON_FLAG = 1<<29; public static final int STATE_GPS_ON_FLAG = 1<<28; - public static final int STATE_PHONE_IN_CALL_FLAG = 1<<27; - public static final int STATE_PHONE_SCANNING_FLAG = 1<<26; - public static final int STATE_WIFI_ON_FLAG = 1<<25; - public static final int STATE_WIFI_RUNNING_FLAG = 1<<24; - public static final int STATE_WIFI_FULL_LOCK_FLAG = 1<<23; - public static final int STATE_WIFI_SCAN_LOCK_FLAG = 1<<22; - public static final int STATE_WIFI_MULTICAST_ON_FLAG = 1<<21; - public static final int STATE_BLUETOOTH_ON_FLAG = 1<<20; - public static final int STATE_AUDIO_ON_FLAG = 1<<19; - public static final int STATE_VIDEO_ON_FLAG = 1<<18; - public static final int STATE_WAKE_LOCK_FLAG = 1<<17; - public static final int STATE_SENSOR_ON_FLAG = 1<<16; + public static final int STATE_PHONE_SCANNING_FLAG = 1<<27; + public static final int STATE_WIFI_RUNNING_FLAG = 1<<26; + public static final int STATE_WIFI_FULL_LOCK_FLAG = 1<<25; + public static final int STATE_WIFI_SCAN_LOCK_FLAG = 1<<24; + public static final int STATE_WIFI_MULTICAST_ON_FLAG = 1<<23; + // These are on the lower bits used for the command; if they change + // we need to write another int of data. + public static final int STATE_AUDIO_ON_FLAG = 1<<22; + public static final int STATE_VIDEO_ON_FLAG = 1<<21; + public static final int STATE_SCREEN_ON_FLAG = 1<<20; + public static final int STATE_BATTERY_PLUGGED_FLAG = 1<<19; + public static final int STATE_PHONE_IN_CALL_FLAG = 1<<18; + public static final int STATE_WIFI_ON_FLAG = 1<<17; + public static final int STATE_BLUETOOTH_ON_FLAG = 1<<16; public static final int MOST_INTERESTING_STATES = STATE_BATTERY_PLUGGED_FLAG | STATE_SCREEN_ON_FLAG @@ -466,16 +476,7 @@ public abstract class BatteryStats implements Parcelable { public HistoryItem(long time, Parcel src) { this.time = time; - int bat = src.readInt(); - cmd = (byte)(bat&0xff); - batteryLevel = (byte)((bat>>8)&0xff); - batteryStatus = (byte)((bat>>16)&0xf); - batteryHealth = (byte)((bat>>20)&0xf); - batteryPlugType = (byte)((bat>>24)&0xf); - bat = src.readInt(); - batteryTemperature = (char)(bat&0xffff); - batteryVoltage = (char)((bat>>16)&0xffff); - states = src.readInt(); + readFromParcel(src); } public int describeContents() { @@ -495,6 +496,174 @@ public abstract class BatteryStats implements Parcelable { dest.writeInt(bat); dest.writeInt(states); } + + private void readFromParcel(Parcel src) { + int bat = src.readInt(); + cmd = (byte)(bat&0xff); + batteryLevel = (byte)((bat>>8)&0xff); + batteryStatus = (byte)((bat>>16)&0xf); + batteryHealth = (byte)((bat>>20)&0xf); + batteryPlugType = (byte)((bat>>24)&0xf); + bat = src.readInt(); + batteryTemperature = (char)(bat&0xffff); + batteryVoltage = (char)((bat>>16)&0xffff); + states = src.readInt(); + } + + // Part of initial delta int that specifies the time delta. + static final int DELTA_TIME_MASK = 0x3ffff; + static final int DELTA_TIME_ABS = 0x3fffd; // Following is an entire abs update. + static final int DELTA_TIME_INT = 0x3fffe; // The delta is a following int + static final int DELTA_TIME_LONG = 0x3ffff; // The delta is a following long + // Part of initial delta int holding the command code. + static final int DELTA_CMD_MASK = 0x3; + static final int DELTA_CMD_SHIFT = 18; + // Flag in delta int: a new battery level int follows. + static final int DELTA_BATTERY_LEVEL_FLAG = 1<<20; + // Flag in delta int: a new full state and battery status int follows. + static final int DELTA_STATE_FLAG = 1<<21; + static final int DELTA_STATE_MASK = 0xffc00000; + + public void writeDelta(Parcel dest, HistoryItem last) { + if (last == null || last.cmd != CMD_UPDATE) { + dest.writeInt(DELTA_TIME_ABS); + writeToParcel(dest, 0); + return; + } + + final long deltaTime = time - last.time; + final int lastBatteryLevelInt = last.buildBatteryLevelInt(); + final int lastStateInt = last.buildStateInt(); + + int deltaTimeToken; + if (deltaTime < 0 || deltaTime > Integer.MAX_VALUE) { + deltaTimeToken = DELTA_TIME_LONG; + } else if (deltaTime >= DELTA_TIME_ABS) { + deltaTimeToken = DELTA_TIME_INT; + } else { + deltaTimeToken = (int)deltaTime; + } + int firstToken = deltaTimeToken + | (cmd<<DELTA_CMD_SHIFT) + | (states&DELTA_STATE_MASK); + final int batteryLevelInt = buildBatteryLevelInt(); + final boolean batteryLevelIntChanged = batteryLevelInt != lastBatteryLevelInt; + if (batteryLevelIntChanged) { + firstToken |= DELTA_BATTERY_LEVEL_FLAG; + } + final int stateInt = buildStateInt(); + final boolean stateIntChanged = stateInt != lastStateInt; + if (stateIntChanged) { + firstToken |= DELTA_STATE_FLAG; + } + dest.writeInt(firstToken); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: firstToken=0x" + Integer.toHexString(firstToken) + + " deltaTime=" + deltaTime); + + if (deltaTimeToken >= DELTA_TIME_INT) { + if (deltaTimeToken == DELTA_TIME_INT) { + if (DEBUG) Slog.i(TAG, "WRITE DELTA: int deltaTime=" + (int)deltaTime); + dest.writeInt((int)deltaTime); + } else { + if (DEBUG) Slog.i(TAG, "WRITE DELTA: long deltaTime=" + deltaTime); + dest.writeLong(deltaTime); + } + } + if (batteryLevelIntChanged) { + dest.writeInt(batteryLevelInt); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: batteryToken=0x" + + Integer.toHexString(batteryLevelInt) + + " batteryLevel=" + batteryLevel + + " batteryTemp=" + (int)batteryTemperature + + " batteryVolt=" + (int)batteryVoltage); + } + if (stateIntChanged) { + dest.writeInt(stateInt); + if (DEBUG) Slog.i(TAG, "WRITE DELTA: stateToken=0x" + + Integer.toHexString(stateInt) + + " batteryStatus=" + batteryStatus + + " batteryHealth=" + batteryHealth + + " batteryPlugType=" + batteryPlugType + + " states=0x" + Integer.toHexString(states)); + } + } + + private int buildBatteryLevelInt() { + return ((((int)batteryLevel)<<24)&0xff000000) + | ((((int)batteryTemperature)<<14)&0x00ffc000) + | (((int)batteryVoltage)&0x00003fff); + } + + private int buildStateInt() { + return ((((int)batteryStatus)<<28)&0xf0000000) + | ((((int)batteryHealth)<<24)&0x0f000000) + | ((((int)batteryPlugType)<<22)&0x00c00000) + | (states&(~DELTA_STATE_MASK)); + } + + public void readDelta(Parcel src) { + int firstToken = src.readInt(); + int deltaTimeToken = firstToken&DELTA_TIME_MASK; + cmd = (byte)((firstToken>>DELTA_CMD_SHIFT)&DELTA_CMD_MASK); + if (DEBUG) Slog.i(TAG, "READ DELTA: firstToken=0x" + Integer.toHexString(firstToken) + + " deltaTimeToken=" + deltaTimeToken); + + if (deltaTimeToken < DELTA_TIME_ABS) { + time += deltaTimeToken; + } else if (deltaTimeToken == DELTA_TIME_ABS) { + time = src.readLong(); + readFromParcel(src); + return; + } else if (deltaTimeToken == DELTA_TIME_INT) { + int delta = src.readInt(); + time += delta; + if (DEBUG) Slog.i(TAG, "READ DELTA: time delta=" + delta + " new time=" + time); + } else { + long delta = src.readLong(); + if (DEBUG) Slog.i(TAG, "READ DELTA: time delta=" + delta + " new time=" + time); + time += delta; + } + + if ((firstToken&DELTA_BATTERY_LEVEL_FLAG) != 0) { + int batteryLevelInt = src.readInt(); + batteryLevel = (byte)((batteryLevelInt>>24)&0xff); + batteryTemperature = (char)((batteryLevelInt>>14)&0x3ff); + batteryVoltage = (char)(batteryLevelInt&0x3fff); + if (DEBUG) Slog.i(TAG, "READ DELTA: batteryToken=0x" + + Integer.toHexString(batteryLevelInt) + + " batteryLevel=" + batteryLevel + + " batteryTemp=" + (int)batteryTemperature + + " batteryVolt=" + (int)batteryVoltage); + } + + if ((firstToken&DELTA_STATE_FLAG) != 0) { + int stateInt = src.readInt(); + states = (firstToken&DELTA_STATE_MASK) | (stateInt&(~DELTA_STATE_MASK)); + batteryStatus = (byte)((stateInt>>28)&0xf); + batteryHealth = (byte)((stateInt>>24)&0xf); + batteryPlugType = (byte)((stateInt>>22)&0x3); + if (DEBUG) Slog.i(TAG, "READ DELTA: stateToken=0x" + + Integer.toHexString(stateInt) + + " batteryStatus=" + batteryStatus + + " batteryHealth=" + batteryHealth + + " batteryPlugType=" + batteryPlugType + + " states=0x" + Integer.toHexString(states)); + } else { + states = (firstToken&DELTA_STATE_MASK) | (states&(~DELTA_STATE_MASK)); + } + } + + public void clear() { + time = 0; + cmd = CMD_NULL; + batteryLevel = 0; + batteryStatus = 0; + batteryHealth = 0; + batteryPlugType = 0; + batteryTemperature = 0; + batteryVoltage = 0; + states = 0; + } public void setTo(HistoryItem o) { time = o.time; @@ -556,11 +725,14 @@ public abstract class BatteryStats implements Parcelable { public abstract boolean getNextHistoryLocked(HistoryItem out); - /** - * Return the current history of battery state changes. - */ - public abstract HistoryItem getHistory(); - + public abstract void finishIteratingHistoryLocked(); + + public abstract boolean startIteratingOldHistoryLocked(); + + public abstract boolean getNextOldHistoryLocked(HistoryItem out); + + public abstract void finishIteratingOldHistoryLocked(); + /** * Return the base time offset for the battery history. */ @@ -1729,7 +1901,7 @@ public abstract class BatteryStats implements Parcelable { } } - void printBitDescriptions(PrintWriter pw, int oldval, int newval, BitDescription[] descriptions) { + static void printBitDescriptions(PrintWriter pw, int oldval, int newval, BitDescription[] descriptions) { int diff = oldval ^ newval; if (diff == 0) return; for (int i=0; i<descriptions.length; i++) { @@ -1753,6 +1925,125 @@ public abstract class BatteryStats implements Parcelable { } } + public void prepareForDumpLocked() { + } + + public static class HistoryPrinter { + int oldState = 0; + int oldStatus = -1; + int oldHealth = -1; + int oldPlug = -1; + int oldTemp = -1; + int oldVolt = -1; + + public void printNextItem(PrintWriter pw, HistoryItem rec, long now) { + pw.print(" "); + TimeUtils.formatDuration(rec.time-now, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN); + pw.print(" "); + if (rec.cmd == HistoryItem.CMD_START) { + pw.println(" START"); + } else if (rec.cmd == HistoryItem.CMD_OVERFLOW) { + pw.println(" *OVERFLOW*"); + } else { + if (rec.batteryLevel < 10) pw.print("00"); + else if (rec.batteryLevel < 100) pw.print("0"); + pw.print(rec.batteryLevel); + pw.print(" "); + if (rec.states < 0x10) pw.print("0000000"); + else if (rec.states < 0x100) pw.print("000000"); + else if (rec.states < 0x1000) pw.print("00000"); + else if (rec.states < 0x10000) pw.print("0000"); + else if (rec.states < 0x100000) pw.print("000"); + else if (rec.states < 0x1000000) pw.print("00"); + else if (rec.states < 0x10000000) pw.print("0"); + pw.print(Integer.toHexString(rec.states)); + if (oldStatus != rec.batteryStatus) { + oldStatus = rec.batteryStatus; + pw.print(" status="); + switch (oldStatus) { + case BatteryManager.BATTERY_STATUS_UNKNOWN: + pw.print("unknown"); + break; + case BatteryManager.BATTERY_STATUS_CHARGING: + pw.print("charging"); + break; + case BatteryManager.BATTERY_STATUS_DISCHARGING: + pw.print("discharging"); + break; + case BatteryManager.BATTERY_STATUS_NOT_CHARGING: + pw.print("not-charging"); + break; + case BatteryManager.BATTERY_STATUS_FULL: + pw.print("full"); + break; + default: + pw.print(oldStatus); + break; + } + } + if (oldHealth != rec.batteryHealth) { + oldHealth = rec.batteryHealth; + pw.print(" health="); + switch (oldHealth) { + case BatteryManager.BATTERY_HEALTH_UNKNOWN: + pw.print("unknown"); + break; + case BatteryManager.BATTERY_HEALTH_GOOD: + pw.print("good"); + break; + case BatteryManager.BATTERY_HEALTH_OVERHEAT: + pw.print("overheat"); + break; + case BatteryManager.BATTERY_HEALTH_DEAD: + pw.print("dead"); + break; + case BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE: + pw.print("over-voltage"); + break; + case BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE: + pw.print("failure"); + break; + default: + pw.print(oldHealth); + break; + } + } + if (oldPlug != rec.batteryPlugType) { + oldPlug = rec.batteryPlugType; + pw.print(" plug="); + switch (oldPlug) { + case 0: + pw.print("none"); + break; + case BatteryManager.BATTERY_PLUGGED_AC: + pw.print("ac"); + break; + case BatteryManager.BATTERY_PLUGGED_USB: + pw.print("usb"); + break; + default: + pw.print(oldPlug); + break; + } + } + if (oldTemp != rec.batteryTemperature) { + oldTemp = rec.batteryTemperature; + pw.print(" temp="); + pw.print(oldTemp); + } + if (oldVolt != rec.batteryVoltage) { + oldVolt = rec.batteryVoltage; + pw.print(" volt="); + pw.print(oldVolt); + } + printBitDescriptions(pw, oldState, rec.states, + HISTORY_STATE_DESCRIPTIONS); + pw.println(); + } + oldState = rec.states; + } + } + /** * Dumps a human-readable summary of the battery statistics to the given PrintWriter. * @@ -1760,122 +2051,28 @@ public abstract class BatteryStats implements Parcelable { */ @SuppressWarnings("unused") public void dumpLocked(PrintWriter pw) { + prepareForDumpLocked(); + + long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); + final HistoryItem rec = new HistoryItem(); if (startIteratingHistoryLocked()) { pw.println("Battery History:"); - long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); - int oldState = 0; - int oldStatus = -1; - int oldHealth = -1; - int oldPlug = -1; - int oldTemp = -1; - int oldVolt = -1; + HistoryPrinter hprinter = new HistoryPrinter(); while (getNextHistoryLocked(rec)) { - pw.print(" "); - TimeUtils.formatDuration(rec.time-now, pw, TimeUtils.HUNDRED_DAY_FIELD_LEN); - pw.print(" "); - if (rec.cmd == HistoryItem.CMD_START) { - pw.println(" START"); - } else if (rec.cmd == HistoryItem.CMD_OVERFLOW) { - pw.println(" *OVERFLOW*"); - } else { - if (rec.batteryLevel < 10) pw.print("00"); - else if (rec.batteryLevel < 100) pw.print("0"); - pw.print(rec.batteryLevel); - pw.print(" "); - if (rec.states < 0x10) pw.print("0000000"); - else if (rec.states < 0x100) pw.print("000000"); - else if (rec.states < 0x1000) pw.print("00000"); - else if (rec.states < 0x10000) pw.print("0000"); - else if (rec.states < 0x100000) pw.print("000"); - else if (rec.states < 0x1000000) pw.print("00"); - else if (rec.states < 0x10000000) pw.print("0"); - pw.print(Integer.toHexString(rec.states)); - if (oldStatus != rec.batteryStatus) { - oldStatus = rec.batteryStatus; - pw.print(" status="); - switch (oldStatus) { - case BatteryManager.BATTERY_STATUS_UNKNOWN: - pw.print("unknown"); - break; - case BatteryManager.BATTERY_STATUS_CHARGING: - pw.print("charging"); - break; - case BatteryManager.BATTERY_STATUS_DISCHARGING: - pw.print("discharging"); - break; - case BatteryManager.BATTERY_STATUS_NOT_CHARGING: - pw.print("not-charging"); - break; - case BatteryManager.BATTERY_STATUS_FULL: - pw.print("full"); - break; - default: - pw.print(oldStatus); - break; - } - } - if (oldHealth != rec.batteryHealth) { - oldHealth = rec.batteryHealth; - pw.print(" health="); - switch (oldHealth) { - case BatteryManager.BATTERY_HEALTH_UNKNOWN: - pw.print("unknown"); - break; - case BatteryManager.BATTERY_HEALTH_GOOD: - pw.print("good"); - break; - case BatteryManager.BATTERY_HEALTH_OVERHEAT: - pw.print("overheat"); - break; - case BatteryManager.BATTERY_HEALTH_DEAD: - pw.print("dead"); - break; - case BatteryManager.BATTERY_HEALTH_OVER_VOLTAGE: - pw.print("over-voltage"); - break; - case BatteryManager.BATTERY_HEALTH_UNSPECIFIED_FAILURE: - pw.print("failure"); - break; - default: - pw.print(oldHealth); - break; - } - } - if (oldPlug != rec.batteryPlugType) { - oldPlug = rec.batteryPlugType; - pw.print(" plug="); - switch (oldPlug) { - case 0: - pw.print("none"); - break; - case BatteryManager.BATTERY_PLUGGED_AC: - pw.print("ac"); - break; - case BatteryManager.BATTERY_PLUGGED_USB: - pw.print("usb"); - break; - default: - pw.print(oldPlug); - break; - } - } - if (oldTemp != rec.batteryTemperature) { - oldTemp = rec.batteryTemperature; - pw.print(" temp="); - pw.print(oldTemp); - } - if (oldVolt != rec.batteryVoltage) { - oldVolt = rec.batteryVoltage; - pw.print(" volt="); - pw.print(oldVolt); - } - printBitDescriptions(pw, oldState, rec.states, - HISTORY_STATE_DESCRIPTIONS); - pw.println(); - } - oldState = rec.states; + hprinter.printNextItem(pw, rec, now); + } + finishIteratingHistoryLocked(); + pw.println(""); + } + + if (startIteratingOldHistoryLocked()) { + pw.println("Old battery History:"); + HistoryPrinter hprinter = new HistoryPrinter(); + while (getNextOldHistoryLocked(rec)) { + hprinter.printNextItem(pw, rec, now); } + finishIteratingOldHistoryLocked(); pw.println(""); } @@ -1917,6 +2114,8 @@ public abstract class BatteryStats implements Parcelable { @SuppressWarnings("unused") public void dumpCheckinLocked(PrintWriter pw, String[] args, List<ApplicationInfo> apps) { + prepareForDumpLocked(); + boolean isUnpluggedOnly = false; for (String arg : args) { diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index ae1e1c2..c25ebb7 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -16,7 +16,6 @@ package android.os; -import android.util.Config; import android.util.Log; import java.io.FileDescriptor; @@ -291,7 +290,7 @@ public class Binder implements IBinder { */ public final boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { - if (Config.LOGV) Log.v("Binder", "Transact: " + code + " to " + this); + if (false) Log.v("Binder", "Transact: " + code + " to " + this); if (data != null) { data.setDataPosition(0); } @@ -413,7 +412,7 @@ final class BinderProxy implements IBinder { private native final void destroy(); private static final void sendDeathNotice(DeathRecipient recipient) { - if (Config.LOGV) Log.v("JavaBinder", "sendDeathNotice to " + recipient); + if (false) Log.v("JavaBinder", "sendDeathNotice to " + recipient); try { recipient.binderDied(); } diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 6257e95..8ff5beb 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -245,6 +245,11 @@ public class Build { * switch them in to screen size compatibility mode.</p> */ public static final int HONEYCOMB_MR2 = 13; + + /** + * Current version under development. + */ + public static final int ICE_CREAM_SANDWICH = CUR_DEVELOPMENT; } /** The type of build, like "user" or "eng". */ diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 87aeccb..ba69246 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -18,7 +18,6 @@ package android.os; import com.android.internal.util.TypedProperties; -import android.util.Config; import android.util.Log; import java.io.FileDescriptor; @@ -1031,7 +1030,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo * Load the debug properties from the standard files into debugProperties. */ static { - if (Config.DEBUG) { + if (false) { final String TAG = "DebugProperties"; final String[] files = { "/system/debug.prop", "/debug.prop", "/data/debug.prop" }; final TypedProperties tp = new TypedProperties(); @@ -1157,10 +1156,10 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo /** * Reflectively sets static fields of a class based on internal debugging - * properties. This method is a no-op if android.util.Config.DEBUG is + * properties. This method is a no-op if false is * false. * <p> - * <strong>NOTE TO APPLICATION DEVELOPERS</strong>: Config.DEBUG will + * <strong>NOTE TO APPLICATION DEVELOPERS</strong>: false will * always be false in release builds. This API is typically only useful * for platform developers. * </p> @@ -1211,7 +1210,7 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo * the internal debugging property value. */ public static void setFieldsOn(Class<?> cl, boolean partial) { - if (Config.DEBUG) { + if (false) { if (debugProperties != null) { /* Only look for fields declared directly by the class, * so we don't mysteriously change static fields in superclasses. diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index 1f3f6d9..11f9445 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -16,13 +16,13 @@ package android.os; -import java.io.File; - import android.content.res.Resources; import android.os.storage.IMountService; import android.os.storage.StorageVolume; import android.util.Log; +import java.io.File; + /** * Provides access to environment variables. */ @@ -113,18 +113,18 @@ public class Environment { = getDirectory("ANDROID_SECURE_DATA", "/data/secure"); private static final File EXTERNAL_STORAGE_DIRECTORY - = getDirectory("EXTERNAL_STORAGE", "/sdcard"); + = getDirectory("EXTERNAL_STORAGE", "/mnt/sdcard"); private static final File EXTERNAL_STORAGE_ANDROID_DATA_DIRECTORY - = new File (new File(getDirectory("EXTERNAL_STORAGE", "/sdcard"), + = new File (new File(getDirectory("EXTERNAL_STORAGE", "/mnt/sdcard"), "Android"), "data"); private static final File EXTERNAL_STORAGE_ANDROID_MEDIA_DIRECTORY - = new File (new File(getDirectory("EXTERNAL_STORAGE", "/sdcard"), + = new File (new File(getDirectory("EXTERNAL_STORAGE", "/mnt/sdcard"), "Android"), "media"); private static final File EXTERNAL_STORAGE_ANDROID_OBB_DIRECTORY - = new File (new File(getDirectory("EXTERNAL_STORAGE", "/sdcard"), + = new File (new File(getDirectory("EXTERNAL_STORAGE", "/mnt/sdcard"), "Android"), "obb"); private static final File DOWNLOAD_CACHE_DIRECTORY @@ -357,54 +357,54 @@ public class Environment { } /** - * getExternalStorageState() returns MEDIA_REMOVED if the media is not present. + * {@link #getExternalStorageState()} returns MEDIA_REMOVED if the media is not present. */ public static final String MEDIA_REMOVED = "removed"; /** - * getExternalStorageState() returns MEDIA_UNMOUNTED if the media is present + * {@link #getExternalStorageState()} returns MEDIA_UNMOUNTED if the media is present * but not mounted. */ public static final String MEDIA_UNMOUNTED = "unmounted"; /** - * getExternalStorageState() returns MEDIA_CHECKING if the media is present + * {@link #getExternalStorageState()} returns MEDIA_CHECKING if the media is present * and being disk-checked */ public static final String MEDIA_CHECKING = "checking"; /** - * getExternalStorageState() returns MEDIA_NOFS if the media is present + * {@link #getExternalStorageState()} returns MEDIA_NOFS if the media is present * but is blank or is using an unsupported filesystem */ public static final String MEDIA_NOFS = "nofs"; /** - * getExternalStorageState() returns MEDIA_MOUNTED if the media is present + * {@link #getExternalStorageState()} returns MEDIA_MOUNTED if the media is present * and mounted at its mount point with read/write access. */ public static final String MEDIA_MOUNTED = "mounted"; /** - * getExternalStorageState() returns MEDIA_MOUNTED_READ_ONLY if the media is present + * {@link #getExternalStorageState()} returns MEDIA_MOUNTED_READ_ONLY if the media is present * and mounted at its mount point with read only access. */ public static final String MEDIA_MOUNTED_READ_ONLY = "mounted_ro"; /** - * getExternalStorageState() returns MEDIA_SHARED if the media is present + * {@link #getExternalStorageState()} returns MEDIA_SHARED if the media is present * not mounted, and shared via USB mass storage. */ public static final String MEDIA_SHARED = "shared"; /** - * getExternalStorageState() returns MEDIA_BAD_REMOVAL if the media was + * {@link #getExternalStorageState()} returns MEDIA_BAD_REMOVAL if the media was * removed before it was unmounted. */ public static final String MEDIA_BAD_REMOVAL = "bad_removal"; /** - * getExternalStorageState() returns MEDIA_UNMOUNTABLE if the media is present + * {@link #getExternalStorageState()} returns MEDIA_UNMOUNTABLE if the media is present * but cannot be mounted. Typically this happens if the file system on the * media is corrupted. */ diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index 632daa1..215e836 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -21,6 +21,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.util.regex.Pattern; @@ -228,6 +229,22 @@ public class FileUtils { } } + /** + * Writes string to file. Basically same as "echo -n $string > $filename" + * + * @param filename + * @param string + * @throws IOException + */ + public static void stringToFile(String filename, String string) throws IOException { + FileWriter out = new FileWriter(filename); + try { + out.write(string); + } finally { + out.close(); + } + } + /** * Computes the checksum of a file using the CRC32 checksum routine. * The value of the checksum is returned. diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index 5a245f8..f17a6f2 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -19,6 +19,7 @@ package android.os; import android.net.InterfaceConfiguration; import android.net.INetworkManagementEventObserver; +import android.net.NetworkStats; import android.net.RouteInfo; import android.net.wifi.WifiConfiguration; @@ -82,7 +83,6 @@ interface INetworkManagementService ** TETHERING RELATED **/ - /** * Returns true if IP forwarding is enabled */ @@ -198,17 +198,29 @@ interface INetworkManagementService void setAccessPoint(in WifiConfiguration wifiConfig, String wlanIface, String softapIface); /** - * Read number of bytes sent over an interface + ** DATA USAGE RELATED + **/ + + /** + * Return global network statistics summarized at an interface level, + * without any UID-level granularity. + */ + NetworkStats getNetworkStatsSummary(); + + /** + * Return detailed network statistics with UID-level granularity, + * including interface and tag details. */ - long getInterfaceTxCounter(String iface); + NetworkStats getNetworkStatsDetail(); /** - * Read number of bytes received over an interface + * Return detailed network statistics for the requested UID, + * including interface and tag details. */ - long getInterfaceRxCounter(String iface); + NetworkStats getNetworkStatsUidDetail(int uid); /** - * Configures bandwidth throttling on an interface + * Configures bandwidth throttling on an interface. */ void setInterfaceThrottle(String iface, int rxKbps, int txKbps); diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java index ccf642c..3edd692 100644 --- a/core/java/android/os/Looper.java +++ b/core/java/android/os/Looper.java @@ -16,7 +16,6 @@ package android.os; -import android.util.Config; import android.util.Log; import android.util.Printer; import android.util.PrefixPrinter; diff --git a/core/java/android/os/MemoryFile.java b/core/java/android/os/MemoryFile.java index f82702a..e8148f7 100644 --- a/core/java/android/os/MemoryFile.java +++ b/core/java/android/os/MemoryFile.java @@ -28,7 +28,7 @@ import java.io.OutputStream; * MemoryFile is a wrapper for the Linux ashmem driver. * MemoryFiles are backed by shared memory, which can be optionally * set to be purgeable. - * Purgeable files may have their contents reclaimed by the kernel + * Purgeable files may have their contents reclaimed by the kernel * in low memory conditions (only if allowPurging is set to true). * After a file is purged, attempts to read or write the file will * cause an IOException to be thrown. @@ -126,7 +126,7 @@ public class MemoryFile close(); } } - + /** * Returns the length of the memory file. * @@ -190,7 +190,7 @@ public class MemoryFile * @return number of bytes read. * @throws IOException if the memory file has been purged or deactivated. */ - public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count) + public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count) throws IOException { if (isDeactivated()) { throw new IOException("Can't read from deactivated memory file."); @@ -330,6 +330,7 @@ public class MemoryFile @Override public void write(byte buffer[], int offset, int count) throws IOException { writeBytes(buffer, offset, mOffset, count); + mOffset += count; } @Override diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java index bb07825..a658fc4 100644 --- a/core/java/android/os/MessageQueue.java +++ b/core/java/android/os/MessageQueue.java @@ -17,7 +17,6 @@ package android.os; import android.util.AndroidRuntimeException; -import android.util.Config; import android.util.Log; import java.util.ArrayList; @@ -128,7 +127,7 @@ public class MessageQueue { mBlocked = false; mMessages = msg.next; msg.next = null; - if (Config.LOGV) Log.v("MessageQueue", "Returning message: " + msg); + if (false) Log.v("MessageQueue", "Returning message: " + msg); msg.markInUse(); return msg; } else { diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 0f1354b..3ea3f56 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -21,6 +21,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.net.DatagramSocket; import java.net.Socket; /** @@ -35,64 +36,64 @@ public class ParcelFileDescriptor implements Parcelable { //consider ParcelFileDescriptor A(fileDescriptor fd), ParcelFileDescriptor B(A) //in this particular case fd.close might be invoked twice. private final ParcelFileDescriptor mParcelDescriptor; - + /** * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied * and this file doesn't already exist, then create the file with * permissions such that any application can read it. */ public static final int MODE_WORLD_READABLE = 0x00000001; - + /** * For use with {@link #open}: if {@link #MODE_CREATE} has been supplied * and this file doesn't already exist, then create the file with * permissions such that any application can write it. */ public static final int MODE_WORLD_WRITEABLE = 0x00000002; - + /** * For use with {@link #open}: open the file with read-only access. */ public static final int MODE_READ_ONLY = 0x10000000; - + /** * For use with {@link #open}: open the file with write-only access. */ public static final int MODE_WRITE_ONLY = 0x20000000; - + /** * For use with {@link #open}: open the file with read and write access. */ public static final int MODE_READ_WRITE = 0x30000000; - + /** * For use with {@link #open}: create the file if it doesn't already exist. */ public static final int MODE_CREATE = 0x08000000; - + /** * For use with {@link #open}: erase contents of file when opening. */ public static final int MODE_TRUNCATE = 0x04000000; - + /** * For use with {@link #open}: append to end of file while writing. */ public static final int MODE_APPEND = 0x02000000; - + /** * Create a new ParcelFileDescriptor accessing a given file. - * + * * @param file The file to be opened. * @param mode The desired access mode, must be one of * {@link #MODE_READ_ONLY}, {@link #MODE_WRITE_ONLY}, or * {@link #MODE_READ_WRITE}; may also be any combination of * {@link #MODE_CREATE}, {@link #MODE_TRUNCATE}, * {@link #MODE_WORLD_READABLE}, and {@link #MODE_WORLD_WRITEABLE}. - * + * * @return Returns a new ParcelFileDescriptor pointing to the given * file. - * + * * @throws FileNotFoundException Throws FileNotFoundException if the given * file does not exist or can not be opened with the requested mode. */ @@ -106,12 +107,12 @@ public class ParcelFileDescriptor implements Parcelable { security.checkWrite(path); } } - + if ((mode&MODE_READ_WRITE) == 0) { throw new IllegalArgumentException( "Must specify MODE_READ_ONLY, MODE_WRITE_ONLY, or MODE_READ_WRITE"); } - + FileDescriptor fd = Parcel.openFileDescriptor(path, mode); return fd != null ? new ParcelFileDescriptor(fd) : null; } @@ -176,12 +177,23 @@ public class ParcelFileDescriptor implements Parcelable { * specified Socket. */ public static ParcelFileDescriptor fromSocket(Socket socket) { - FileDescriptor fd = getFileDescriptorFromSocket(socket); + FileDescriptor fd = socket.getFileDescriptor$(); return fd != null ? new ParcelFileDescriptor(fd) : null; } - // Extracts the file descriptor from the specified socket and returns it untouched - private static native FileDescriptor getFileDescriptorFromSocket(Socket socket); + /** + * Create a new ParcelFileDescriptor from the specified DatagramSocket. + * + * @param datagramSocket The DatagramSocket whose FileDescriptor is used + * to create a new ParcelFileDescriptor. + * + * @return A new ParcelFileDescriptor with the FileDescriptor of the + * specified DatagramSocket. + */ + public static ParcelFileDescriptor fromDatagramSocket(DatagramSocket datagramSocket) { + FileDescriptor fd = datagramSocket.getFileDescriptor$(); + return fd != null ? new ParcelFileDescriptor(fd) : null; + } /** * Create two ParcelFileDescriptors structured as a data pipe. The first @@ -223,26 +235,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 @@ -254,9 +266,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 @@ -276,11 +288,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. */ @@ -297,7 +309,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 @@ -305,7 +317,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; @@ -320,7 +332,7 @@ public class ParcelFileDescriptor implements Parcelable { } } } - + /** * An OutputStream you can create on a ParcelFileDescriptor, which will * take care of calling {@link ParcelFileDescriptor#close @@ -328,7 +340,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; @@ -343,12 +355,12 @@ public class ParcelFileDescriptor implements Parcelable { } } } - + @Override public String toString() { return "{ParcelFileDescriptor: " + mFileDescriptor + "}"; } - + @Override protected void finalize() throws Throwable { try { @@ -359,13 +371,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) { @@ -374,7 +386,7 @@ public class ParcelFileDescriptor implements Parcelable { mFileDescriptor = descriptor; mParcelDescriptor = null; } - + /* Parcelable interface */ public int describeContents() { return Parcelable.CONTENTS_FILE_DESCRIPTOR; diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 78275a4..a17983a 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -97,7 +97,8 @@ import android.util.Log; * </tbody> * </table> * - * + * Any application using a WakeLock must request the {@code android.permission.WAKE_LOCK} + * permission in an {@code <uses-permission>} element of the application's manifest. */ public class PowerManager { @@ -199,8 +200,11 @@ public class PowerManager /** * Class lets you say that you need to have the device on. - * - * <p>Call release when you are done and don't need the lock anymore. + * <p> + * Call release when you are done and don't need the lock anymore. + * <p> + * Any application using a WakeLock must request the {@code android.permission.WAKE_LOCK} + * permission in an {@code <uses-permission>} element of the application's manifest. */ public class WakeLock { @@ -257,16 +261,10 @@ public class PowerManager public void acquire() { synchronized (mToken) { - if (!mRefCounted || mCount++ == 0) { - try { - mService.acquireWakeLock(mFlags, mToken, mTag, mWorkSource); - } catch (RemoteException e) { - } - mHeld = true; - } + acquireLocked(); } } - + /** * Makes sure the device is on at the level you asked when you created * the wake lock. The lock will be released after the given timeout. @@ -274,10 +272,22 @@ public class PowerManager * @param timeout Release the lock after the give timeout in milliseconds. */ public void acquire(long timeout) { - acquire(); - mHandler.postDelayed(mReleaser, timeout); + synchronized (mToken) { + acquireLocked(); + mHandler.postDelayed(mReleaser, timeout); + } } + private void acquireLocked() { + if (!mRefCounted || mCount++ == 0) { + mHandler.removeCallbacks(mReleaser); + try { + mService.acquireWakeLock(mFlags, mToken, mTag, mWorkSource); + } catch (RemoteException e) { + } + mHeld = true; + } + } /** * Release your claim to the CPU or screen being on. @@ -286,8 +296,7 @@ public class PowerManager * It may turn off shortly after you release it, or it may not if there * are other wake locks held. */ - public void release() - { + public void release() { release(0); } @@ -302,9 +311,9 @@ public class PowerManager * * {@hide} */ - public void release(int flags) - { + public void release(int flags) { synchronized (mToken) { + mHandler.removeCallbacks(mReleaser); if (!mRefCounted || --mCount == 0) { try { mService.releaseWakeLock(mToken, flags); diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 2bfada0..d475f36 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 */ @@ -628,6 +634,20 @@ public class Process { } /** + * Returns the parent process id for a currently running process. + * @param pid the process id + * @return the parent process id of the process, or -1 if the process is not running. + * @hide + */ + public static final int getParentPid(int pid) { + String[] procStatusLabels = { "PPid:" }; + long[] procStatusValues = new long[1]; + procStatusValues[0] = -1; + Process.readProcLines("/proc/" + pid + "/status", procStatusLabels, procStatusValues); + return (int) procStatusValues[0]; + } + + /** * Set the priority of a thread, based on Linux priorities. * * @param tid The identifier of the thread/process to change. 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/StrictMode.java b/core/java/android/os/StrictMode.java index 1375a29..01c640a 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -1481,6 +1481,13 @@ public final class StrictMode { onVmPolicyViolation(message, originStack); } + /** + * @hide + */ + public static void onWebViewMethodCalledOnWrongThread(Throwable originStack) { + onVmPolicyViolation(null, originStack); + } + // Map from VM violation fingerprint to uptime millis. private static final HashMap<Integer, Long> sLastVmViolationTime = new HashMap<Integer, Long>(); diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java index bc4208a..792e4c1 100644 --- a/core/java/android/os/storage/StorageVolume.java +++ b/core/java/android/os/storage/StorageVolume.java @@ -32,6 +32,7 @@ public class StorageVolume implements Parcelable { private final boolean mRemovable; private final boolean mEmulated; private final int mMtpReserveSpace; + private final boolean mAllowMassStorage; private int mStorageId; // StorageVolume extra for ACTION_MEDIA_REMOVED, ACTION_MEDIA_UNMOUNTED, ACTION_MEDIA_CHECKING, @@ -39,23 +40,25 @@ public class StorageVolume implements Parcelable { // ACTION_MEDIA_BAD_REMOVAL, ACTION_MEDIA_UNMOUNTABLE and ACTION_MEDIA_EJECT broadcasts. public static final String EXTRA_STORAGE_VOLUME = "storage_volume"; - public StorageVolume(String path, String description, - boolean removable, boolean emulated, int mtpReserveSpace) { + public StorageVolume(String path, String description, boolean removable, + boolean emulated, int mtpReserveSpace, boolean allowMassStorage) { mPath = path; mDescription = description; mRemovable = removable; mEmulated = emulated; mMtpReserveSpace = mtpReserveSpace; + mAllowMassStorage = allowMassStorage; } // for parcelling only - private StorageVolume(String path, String description, - boolean removable, boolean emulated, int mtpReserveSpace, int storageId) { + private StorageVolume(String path, String description, boolean removable, + boolean emulated, int mtpReserveSpace, int storageId, boolean allowMassStorage) { mPath = path; mDescription = description; mRemovable = removable; mEmulated = emulated; mMtpReserveSpace = mtpReserveSpace; + mAllowMassStorage = allowMassStorage; mStorageId = storageId; } @@ -130,6 +133,15 @@ public class StorageVolume implements Parcelable { return mMtpReserveSpace; } + /** + * Returns true if this volume can be shared via USB mass storage. + * + * @return whether mass storage is allowed + */ + public boolean allowMassStorage() { + return mAllowMassStorage; + } + @Override public boolean equals(Object obj) { if (obj instanceof StorageVolume && mPath != null) { @@ -158,9 +170,10 @@ public class StorageVolume implements Parcelable { int emulated = in.readInt(); int storageId = in.readInt(); int mtpReserveSpace = in.readInt(); + int allowMassStorage = in.readInt(); return new StorageVolume(path, description, removable == 1, emulated == 1, - mtpReserveSpace, storageId); + mtpReserveSpace, storageId, allowMassStorage == 1); } public StorageVolume[] newArray(int size) { @@ -179,5 +192,6 @@ public class StorageVolume implements Parcelable { parcel.writeInt(mEmulated ? 1 : 0); parcel.writeInt(mStorageId); parcel.writeInt(mMtpReserveSpace); + parcel.writeInt(mAllowMassStorage ? 1 : 0); } } 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/CheckBoxPreference.java b/core/java/android/preference/CheckBoxPreference.java index 2bf6c7b..437e553 100644 --- a/core/java/android/preference/CheckBoxPreference.java +++ b/core/java/android/preference/CheckBoxPreference.java @@ -16,20 +16,11 @@ package android.preference; -import android.app.Service; import android.content.Context; -import android.content.SharedPreferences; import android.content.res.TypedArray; -import android.os.Parcel; -import android.os.Parcelable; import android.util.AttributeSet; import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.accessibility.AccessibilityManager; -import android.widget.CheckBox; import android.widget.Checkable; -import android.widget.TextView; /** * A {@link Preference} that provides checkbox widget @@ -41,31 +32,18 @@ import android.widget.TextView; * @attr ref android.R.styleable#CheckBoxPreference_summaryOn * @attr ref android.R.styleable#CheckBoxPreference_disableDependentsState */ -public class CheckBoxPreference extends Preference { +public class CheckBoxPreference extends TwoStatePreference { - private CharSequence mSummaryOn; - private CharSequence mSummaryOff; - - private boolean mChecked; - private boolean mSendAccessibilityEventViewClickedType; - - private AccessibilityManager mAccessibilityManager; - - private boolean mDisableDependentsState; - public CheckBoxPreference(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.CheckBoxPreference, defStyle, 0); - mSummaryOn = a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOn); - mSummaryOff = a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOff); - mDisableDependentsState = a.getBoolean( - com.android.internal.R.styleable.CheckBoxPreference_disableDependentsState, false); + setSummaryOn(a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOn)); + setSummaryOff(a.getString(com.android.internal.R.styleable.CheckBoxPreference_summaryOff)); + setDisableDependentsState(a.getBoolean( + com.android.internal.R.styleable.CheckBoxPreference_disableDependentsState, false)); a.recycle(); - - mAccessibilityManager = - (AccessibilityManager) getContext().getSystemService(Service.ACCESSIBILITY_SERVICE); } public CheckBoxPreference(Context context, AttributeSet attrs) { @@ -84,246 +62,9 @@ public class CheckBoxPreference extends Preference { if (checkboxView != null && checkboxView instanceof Checkable) { ((Checkable) checkboxView).setChecked(mChecked); - // send an event to announce the value change of the CheckBox and is done here - // because clicking a preference does not immediately change the checked state - // for example when enabling the WiFi - if (mSendAccessibilityEventViewClickedType && - mAccessibilityManager.isEnabled() && - checkboxView.isEnabled()) { - mSendAccessibilityEventViewClickedType = false; - - // we send an event on behalf of the check box because in onBind the latter - // is detached from its parent and such views do not send accessibility events - AccessibilityEvent event = AccessibilityEvent.obtain( - AccessibilityEvent.TYPE_VIEW_CLICKED); - event.setClassName(checkboxView.getClass().getName()); - event.setPackageName(getContext().getPackageName()); - event.setEnabled(checkboxView.isEnabled()); - event.setContentDescription(checkboxView.getContentDescription()); - event.setChecked(((Checkable) checkboxView).isChecked()); - mAccessibilityManager.sendAccessibilityEvent(event); - } - } - - // Sync the summary view - TextView summaryView = (TextView) view.findViewById(com.android.internal.R.id.summary); - if (summaryView != null) { - boolean useDefaultSummary = true; - if (mChecked && mSummaryOn != null) { - summaryView.setText(mSummaryOn); - useDefaultSummary = false; - } else if (!mChecked && mSummaryOff != null) { - summaryView.setText(mSummaryOff); - useDefaultSummary = false; - } - - if (useDefaultSummary) { - final CharSequence summary = getSummary(); - if (summary != null) { - summaryView.setText(summary); - useDefaultSummary = false; - } - } - - int newVisibility = View.GONE; - if (!useDefaultSummary) { - // Someone has written to it - newVisibility = View.VISIBLE; - } - if (newVisibility != summaryView.getVisibility()) { - summaryView.setVisibility(newVisibility); - } - } - } - - @Override - protected void onClick() { - super.onClick(); - - boolean newValue = !isChecked(); - - // in onBindView() an AccessibilityEventViewClickedType is sent to announce the change - // not sending - mSendAccessibilityEventViewClickedType = true; - - if (!callChangeListener(newValue)) { - return; - } - - setChecked(newValue); - } - - /** - * Sets the checked state and saves it to the {@link SharedPreferences}. - * - * @param checked The checked state. - */ - public void setChecked(boolean checked) { - if (mChecked != checked) { - mChecked = checked; - persistBoolean(checked); - notifyDependencyChange(shouldDisableDependents()); - notifyChanged(); - } - } - - /** - * Returns the checked state. - * - * @return The checked state. - */ - public boolean isChecked() { - return mChecked; - } - - @Override - public boolean shouldDisableDependents() { - boolean shouldDisable = mDisableDependentsState ? mChecked : !mChecked; - return shouldDisable || super.shouldDisableDependents(); - } - - /** - * Sets the summary to be shown when checked. - * - * @param summary The summary to be shown when checked. - */ - public void setSummaryOn(CharSequence summary) { - mSummaryOn = summary; - if (isChecked()) { - notifyChanged(); - } - } - - /** - * @see #setSummaryOn(CharSequence) - * @param summaryResId The summary as a resource. - */ - public void setSummaryOn(int summaryResId) { - setSummaryOn(getContext().getString(summaryResId)); - } - - /** - * Returns the summary to be shown when checked. - * @return The summary. - */ - public CharSequence getSummaryOn() { - return mSummaryOn; - } - - /** - * Sets the summary to be shown when unchecked. - * - * @param summary The summary to be shown when unchecked. - */ - public void setSummaryOff(CharSequence summary) { - mSummaryOff = summary; - if (!isChecked()) { - notifyChanged(); - } - } - - /** - * @see #setSummaryOff(CharSequence) - * @param summaryResId The summary as a resource. - */ - public void setSummaryOff(int summaryResId) { - setSummaryOff(getContext().getString(summaryResId)); - } - - /** - * Returns the summary to be shown when unchecked. - * @return The summary. - */ - public CharSequence getSummaryOff() { - return mSummaryOff; - } - - /** - * Returns whether dependents are disabled when this preference is on ({@code true}) - * or when this preference is off ({@code false}). - * - * @return Whether dependents are disabled when this preference is on ({@code true}) - * or when this preference is off ({@code false}). - */ - public boolean getDisableDependentsState() { - return mDisableDependentsState; - } - - /** - * Sets whether dependents are disabled when this preference is on ({@code true}) - * or when this preference is off ({@code false}). - * - * @param disableDependentsState The preference state that should disable dependents. - */ - public void setDisableDependentsState(boolean disableDependentsState) { - mDisableDependentsState = disableDependentsState; - } - - @Override - protected Object onGetDefaultValue(TypedArray a, int index) { - return a.getBoolean(index, false); - } - - @Override - protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { - setChecked(restoreValue ? getPersistedBoolean(mChecked) - : (Boolean) defaultValue); - } - - @Override - protected Parcelable onSaveInstanceState() { - final Parcelable superState = super.onSaveInstanceState(); - if (isPersistent()) { - // No need to save instance state since it's persistent - return superState; - } - - final SavedState myState = new SavedState(superState); - myState.checked = isChecked(); - return myState; - } - - @Override - protected void onRestoreInstanceState(Parcelable state) { - if (state == null || !state.getClass().equals(SavedState.class)) { - // Didn't save state for us in onSaveInstanceState - super.onRestoreInstanceState(state); - return; - } - - SavedState myState = (SavedState) state; - super.onRestoreInstanceState(myState.getSuperState()); - setChecked(myState.checked); - } - - private static class SavedState extends BaseSavedState { - boolean checked; - - public SavedState(Parcel source) { - super(source); - checked = source.readInt() == 1; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeInt(checked ? 1 : 0); + sendAccessibilityEventForView(checkboxView); } - public SavedState(Parcelable superState) { - super(superState); - } - - public static final Parcelable.Creator<SavedState> CREATOR = - new Parcelable.Creator<SavedState>() { - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); - } - - public SavedState[] newArray(int size) { - return new SavedState[size]; - } - }; + syncSummaryView(view); } - } 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 93114ad..b6d1594 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; @@ -514,7 +516,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } } } - + ImageView imageView = (ImageView) view.findViewById(com.android.internal.R.id.icon); if (imageView != null) { if (mIconResId != 0 || mIcon != null) { @@ -590,6 +592,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(); } @@ -603,9 +606,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 8535bd4..14e7bed 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 @@ -496,6 +511,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 +533,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. @@ -934,7 +957,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. @@ -943,7 +967,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); } @@ -953,6 +984,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 @@ -960,14 +1026,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 { @@ -984,16 +1054,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 != null && !(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); @@ -1161,7 +1231,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) { @@ -1207,7 +1277,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..9d46b7a 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,8 @@ 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); + return inflater.inflate(com.android.internal.R.layout.preference_list_fragment, container, + false); } @Override diff --git a/core/java/android/preference/SwitchPreference.java b/core/java/android/preference/SwitchPreference.java new file mode 100644 index 0000000..f681526 --- /dev/null +++ b/core/java/android/preference/SwitchPreference.java @@ -0,0 +1,180 @@ +/* + * 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.preference; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Checkable; +import android.widget.CompoundButton; +import android.widget.Switch; + +/** + * A {@link Preference} that provides a two-state toggleable option. + * <p> + * This preference will store a boolean into the SharedPreferences. + * + * @attr ref android.R.styleable#SwitchPreference_summaryOff + * @attr ref android.R.styleable#SwitchPreference_summaryOn + * @attr ref android.R.styleable#SwitchPreference_switchTextOff + * @attr ref android.R.styleable#SwitchPreference_switchTextOn + * @attr ref android.R.styleable#SwitchPreference_disableDependentsState + */ +public class SwitchPreference extends TwoStatePreference { + // Switch text for on and off states + private CharSequence mSwitchOn; + private CharSequence mSwitchOff; + private final Listener mListener = new Listener(); + + private class Listener implements View.OnClickListener, CompoundButton.OnCheckedChangeListener { + @Override + public void onClick(View v) { + SwitchPreference.this.onClick(); + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + SwitchPreference.this.setChecked(isChecked); + } + } + + /** + * Construct a new SwitchPreference with the given style options. + * + * @param context The Context that will style this preference + * @param attrs Style attributes that differ from the default + * @param defStyle Theme attribute defining the default style options + */ + public SwitchPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.SwitchPreference, defStyle, 0); + setSummaryOn(a.getString(com.android.internal.R.styleable.SwitchPreference_summaryOn)); + setSummaryOff(a.getString(com.android.internal.R.styleable.SwitchPreference_summaryOff)); + setSwitchTextOn(a.getString( + com.android.internal.R.styleable.SwitchPreference_switchTextOn)); + setSwitchTextOff(a.getString( + com.android.internal.R.styleable.SwitchPreference_switchTextOff)); + setDisableDependentsState(a.getBoolean( + com.android.internal.R.styleable.SwitchPreference_disableDependentsState, false)); + a.recycle(); + } + + /** + * Construct a new SwitchPreference with the given style options. + * + * @param context The Context that will style this preference + * @param attrs Style attributes that differ from the default + */ + public SwitchPreference(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.switchPreferenceStyle); + } + + /** + * Construct a new SwitchPreference with default style options. + * + * @param context The Context that will style this preference + */ + public SwitchPreference(Context context) { + this(context, null); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + View checkableView = view.findViewById(com.android.internal.R.id.switchWidget); + if (checkableView != null && checkableView instanceof Checkable) { + ((Checkable) checkableView).setChecked(mChecked); + + sendAccessibilityEventForView(checkableView); + + if (checkableView instanceof Switch) { + final Switch switchView = (Switch) checkableView; + switchView.setTextOn(mSwitchOn); + switchView.setTextOff(mSwitchOff); + switchView.setOnCheckedChangeListener(mListener); + } + + if (checkableView.hasFocusable()) { + // This is a focusable list item. Attach a click handler to toggle the button + // for the rest of the item. + view.setOnClickListener(mListener); + } + } + + syncSummaryView(view); + } + + /** + * Set the text displayed on the switch widget in the on state. + * This should be a very short string; one word if possible. + * + * @param onText Text to display in the on state + */ + public void setSwitchTextOn(CharSequence onText) { + mSwitchOn = onText; + notifyChanged(); + } + + /** + * Set the text displayed on the switch widget in the off state. + * This should be a very short string; one word if possible. + * + * @param offText Text to display in the off state + */ + public void setSwitchTextOff(CharSequence offText) { + mSwitchOff = offText; + notifyChanged(); + } + + /** + * Set the text displayed on the switch widget in the on state. + * This should be a very short string; one word if possible. + * + * @param resId The text as a string resource ID + */ + public void setSwitchTextOn(int resId) { + setSwitchTextOn(getContext().getString(resId)); + } + + /** + * Set the text displayed on the switch widget in the off state. + * This should be a very short string; one word if possible. + * + * @param resId The text as a string resource ID + */ + public void setSwitchTextOff(int resId) { + setSwitchTextOff(getContext().getString(resId)); + } + + /** + * @return The text that will be displayed on the switch widget in the on state + */ + public CharSequence getSwitchTextOn() { + return mSwitchOn; + } + + /** + * @return The text that will be displayed on the switch widget in the off state + */ + public CharSequence getSwitchTextOff() { + return mSwitchOff; + } +} diff --git a/core/java/android/preference/TwoStatePreference.java b/core/java/android/preference/TwoStatePreference.java new file mode 100644 index 0000000..8e21c4c --- /dev/null +++ b/core/java/android/preference/TwoStatePreference.java @@ -0,0 +1,309 @@ +/* + * 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.preference; + +import android.app.Service; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.TypedArray; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.widget.TextView; + +/** + * Common base class for preferences that have two selectable states, persist a + * boolean value in SharedPreferences, and may have dependent preferences that are + * enabled/disabled based on the current state. + */ +public abstract class TwoStatePreference extends Preference { + + private CharSequence mSummaryOn; + private CharSequence mSummaryOff; + boolean mChecked; + private boolean mSendAccessibilityEventViewClickedType; + private AccessibilityManager mAccessibilityManager; + private boolean mDisableDependentsState; + + public TwoStatePreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mAccessibilityManager = + (AccessibilityManager) getContext().getSystemService(Service.ACCESSIBILITY_SERVICE); + } + + public TwoStatePreference(Context context, AttributeSet attrs) { + super(context, attrs); + + mAccessibilityManager = + (AccessibilityManager) getContext().getSystemService(Service.ACCESSIBILITY_SERVICE); + } + + public TwoStatePreference(Context context) { + super(context); + + mAccessibilityManager = + (AccessibilityManager) getContext().getSystemService(Service.ACCESSIBILITY_SERVICE); + } + + @Override + protected void onClick() { + super.onClick(); + + boolean newValue = !isChecked(); + + // in onBindView() an AccessibilityEventViewClickedType is sent to announce the change + // not sending + mSendAccessibilityEventViewClickedType = true; + + if (!callChangeListener(newValue)) { + return; + } + + setChecked(newValue); + } + + /** + * Sets the checked state and saves it to the {@link SharedPreferences}. + * + * @param checked The checked state. + */ + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + persistBoolean(checked); + notifyDependencyChange(shouldDisableDependents()); + notifyChanged(); + } + } + + /** + * Returns the checked state. + * + * @return The checked state. + */ + public boolean isChecked() { + return mChecked; + } + + @Override + public boolean shouldDisableDependents() { + boolean shouldDisable = mDisableDependentsState ? mChecked : !mChecked; + return shouldDisable || super.shouldDisableDependents(); + } + + /** + * Sets the summary to be shown when checked. + * + * @param summary The summary to be shown when checked. + */ + public void setSummaryOn(CharSequence summary) { + mSummaryOn = summary; + if (isChecked()) { + notifyChanged(); + } + } + + /** + * @see #setSummaryOn(CharSequence) + * @param summaryResId The summary as a resource. + */ + public void setSummaryOn(int summaryResId) { + setSummaryOn(getContext().getString(summaryResId)); + } + + /** + * Returns the summary to be shown when checked. + * @return The summary. + */ + public CharSequence getSummaryOn() { + return mSummaryOn; + } + + /** + * Sets the summary to be shown when unchecked. + * + * @param summary The summary to be shown when unchecked. + */ + public void setSummaryOff(CharSequence summary) { + mSummaryOff = summary; + if (!isChecked()) { + notifyChanged(); + } + } + + /** + * @see #setSummaryOff(CharSequence) + * @param summaryResId The summary as a resource. + */ + public void setSummaryOff(int summaryResId) { + setSummaryOff(getContext().getString(summaryResId)); + } + + /** + * Returns the summary to be shown when unchecked. + * @return The summary. + */ + public CharSequence getSummaryOff() { + return mSummaryOff; + } + + /** + * Returns whether dependents are disabled when this preference is on ({@code true}) + * or when this preference is off ({@code false}). + * + * @return Whether dependents are disabled when this preference is on ({@code true}) + * or when this preference is off ({@code false}). + */ + public boolean getDisableDependentsState() { + return mDisableDependentsState; + } + + /** + * Sets whether dependents are disabled when this preference is on ({@code true}) + * or when this preference is off ({@code false}). + * + * @param disableDependentsState The preference state that should disable dependents. + */ + public void setDisableDependentsState(boolean disableDependentsState) { + mDisableDependentsState = disableDependentsState; + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getBoolean(index, false); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + setChecked(restoreValue ? getPersistedBoolean(mChecked) + : (Boolean) defaultValue); + } + + /** + * Send an accessibility event for the given view if appropriate + * @param view View that should send the event + */ + void sendAccessibilityEventForView(View view) { + // send an event to announce the value change of the state. It is done here + // because clicking a preference does not immediately change the checked state + // for example when enabling the WiFi + if (mSendAccessibilityEventViewClickedType && + mAccessibilityManager.isEnabled() && + view.isEnabled()) { + mSendAccessibilityEventViewClickedType = false; + + int eventType = AccessibilityEvent.TYPE_VIEW_CLICKED; + view.sendAccessibilityEventUnchecked(AccessibilityEvent.obtain(eventType)); + } + } + + /** + * Sync a summary view contained within view's subhierarchy with the correct summary text. + * @param view View where a summary should be located + */ + void syncSummaryView(View view) { + // Sync the summary view + TextView summaryView = (TextView) view.findViewById(com.android.internal.R.id.summary); + if (summaryView != null) { + boolean useDefaultSummary = true; + if (mChecked && mSummaryOn != null) { + summaryView.setText(mSummaryOn); + useDefaultSummary = false; + } else if (!mChecked && mSummaryOff != null) { + summaryView.setText(mSummaryOff); + useDefaultSummary = false; + } + + if (useDefaultSummary) { + final CharSequence summary = getSummary(); + if (summary != null) { + summaryView.setText(summary); + useDefaultSummary = false; + } + } + + int newVisibility = View.GONE; + if (!useDefaultSummary) { + // Someone has written to it + newVisibility = View.VISIBLE; + } + if (newVisibility != summaryView.getVisibility()) { + summaryView.setVisibility(newVisibility); + } + } + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + if (isPersistent()) { + // No need to save instance state since it's persistent + return superState; + } + + final SavedState myState = new SavedState(superState); + myState.checked = isChecked(); + return myState; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state == null || !state.getClass().equals(SavedState.class)) { + // Didn't save state for us in onSaveInstanceState + super.onRestoreInstanceState(state); + return; + } + + SavedState myState = (SavedState) state; + super.onRestoreInstanceState(myState.getSuperState()); + setChecked(myState.checked); + } + + static class SavedState extends BaseSavedState { + boolean checked; + + public SavedState(Parcel source) { + super(source); + checked = source.readInt() == 1; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(checked ? 1 : 0); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/core/java/android/provider/BrowserContract.java b/core/java/android/provider/BrowserContract.java index 03bc41a..d678205 100644 --- a/core/java/android/provider/BrowserContract.java +++ b/core/java/android/provider/BrowserContract.java @@ -332,6 +332,13 @@ public class BrowserContract { * <P>Type: TEXT</P> */ public static final String ACCOUNT_TYPE = "account_type"; + + /** + * The ID of the account's root folder. This will be the ID of the folder + * returned when querying {@link Bookmarks#CONTENT_URI_DEFAULT_FOLDER}. + * <P>Type: INTEGER</P> + */ + public static final String ROOT_ID = "root_id"; } /** diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index de71763..bb6ed9c 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -16,6 +16,7 @@ package android.provider; + import android.accounts.Account; import android.app.AlarmManager; import android.app.PendingIntent; @@ -32,7 +33,6 @@ import android.database.Cursor; import android.database.DatabaseUtils; import android.net.Uri; import android.os.RemoteException; -import android.pim.ICalendar; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.format.Time; @@ -44,24 +44,30 @@ import android.util.Log; * @hide */ public final class Calendar { - - public static final String TAG = "Calendar"; + private static final String TAG = "Calendar"; /** - * Broadcast Action: An event reminder. + * Broadcast Action: This is the intent that gets fired when an alarm + * notification needs to be posted for a reminder. */ public static final String EVENT_REMINDER_ACTION = "android.intent.action.EVENT_REMINDER"; /** - * These are the symbolic names for the keys used in the extra data - * passed in the intent for event reminders. + * Intent Extras key: The start time of an event or an instance of a + * recurring event. (milliseconds since epoch) */ public static final String EVENT_BEGIN_TIME = "beginTime"; + + /** + * Intent Extras key: The end time of an event or an instance of a recurring + * event. (milliseconds since epoch) + */ public static final String EVENT_END_TIME = "endTime"; /** - * This must not be changed or horrible, unspeakable things could happen. - * For instance, the Calendar app might break. Also, the db might not work. + * This authority is used for writing to or querying from the calendar + * provider. Note: This is set at first run and cannot be changed without + * breaking apps that access the provider. */ public static final String AUTHORITY = "com.android.calendar"; @@ -73,204 +79,293 @@ public final class Calendar { /** * An optional insert, update or delete URI parameter that allows the caller - * to specify that it is a sync adapter. The default value is false. If true - * the dirty flag is not automatically set and the "syncToNetwork" parameter - * is set to false when calling - * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)}. + * to specify that it is a sync adapter. The default value is false. If set + * to true, the modified row is not marked as "dirty" (needs to be synced) + * and when the provider calls + * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)} + * , the third parameter "syncToNetwork" is set to false. Furthermore, if + * set to true, the caller must also include + * {@link Calendars#ACCOUNT_NAME} and {@link Calendars#ACCOUNT_TYPE} as + * query parameters. + * + * @see Uri.Builder#appendQueryParameter(java.lang.String, java.lang.String) */ public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; - /** - * Generic columns for use by sync adapters. The specific functions of - * these columns are private to the sync adapter. Other clients of the API - * should not attempt to either read or write this column. + * A special account type for calendars not associated with any account. + * Normally calendars that do not match an account on the device will be + * removed. Setting the account_type on a calendar to this will prevent it + * from being wiped if it does not match an existing account. + * + * @see SyncColumns#ACCOUNT_TYPE */ - protected interface BaseSyncColumns { - - /** Generic column for use by sync adapters. */ - public static final String SYNC1 = "sync1"; - /** Generic column for use by sync adapters. */ - public static final String SYNC2 = "sync2"; - /** Generic column for use by sync adapters. */ - public static final String SYNC3 = "sync3"; - /** Generic column for use by sync adapters. */ - public static final String SYNC4 = "sync4"; - /** Generic column for use by sync adapters. */ - public static final String SYNC5 = "sync5"; - } + public static final String ACCOUNT_TYPE_LOCAL = "LOCAL"; /** - * Columns for Sync information used by Calendars and Events tables. + * Generic columns for use by sync adapters. The specific functions of these + * columns are private to the sync adapter. Other clients of the API should + * not attempt to either read or write this column. These columns are + * editable as part of the Calendars Uri, but can only be read if accessed + * through any other Uri. */ - public interface SyncColumns extends BaseSyncColumns { + protected interface CalendarSyncColumns { + + /** - * The account that was used to sync the entry to the device. + * Generic column for use by sync adapters. Column name. * <P>Type: TEXT</P> */ - public static final String _SYNC_ACCOUNT = "_sync_account"; + public static final String CAL_SYNC1 = "cal_sync1"; /** - * The type of the account that was used to sync the entry to the device. + * Generic column for use by sync adapters. Column name. * <P>Type: TEXT</P> */ - public static final String _SYNC_ACCOUNT_TYPE = "_sync_account_type"; + public static final String CAL_SYNC2 = "cal_sync2"; /** - * The unique ID for a row assigned by the sync source. NULL if the row has never been synced. + * Generic column for use by sync adapters. Column name. * <P>Type: TEXT</P> */ - public static final String _SYNC_ID = "_sync_id"; + public static final String CAL_SYNC3 = "cal_sync3"; /** - * The last time, from the sync source's point of view, that this row has been synchronized. - * <P>Type: INTEGER (long)</P> + * Generic column for use by sync adapters. Column name. + * <P>Type: TEXT</P> */ - public static final String _SYNC_TIME = "_sync_time"; + public static final String CAL_SYNC4 = "cal_sync4"; /** - * The version of the row, as assigned by the server. + * Generic column for use by sync adapters. Column name. * <P>Type: TEXT</P> */ - public static final String _SYNC_VERSION = "_sync_version"; + public static final String CAL_SYNC5 = "cal_sync5"; /** - * 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> + * Generic column for use by sync adapters. Column name. + * <P>Type: TEXT</P> */ - public static final String _SYNC_DATA = "_sync_local_id"; + public static final String CAL_SYNC6 = "cal_sync6"; /** - * Used only in persistent providers, and only during merging. - * <P>Type: INTEGER (long)</P> + * Generic column for use by sync adapters. Column name. + * <P>Type: TEXT</P> */ - public static final String _SYNC_MARK = "_sync_mark"; + public static final String CAL_SYNC7 = "cal_sync7"; /** - * Used to indicate that local, unsynced, changes are present. - * <P>Type: INTEGER (long)</P> + * Generic column for use by sync adapters. Column name. + * <P>Type: TEXT</P> + */ + public static final String CAL_SYNC8 = "cal_sync8"; + + /** + * Generic column for use by sync adapters. Column name. + * <P>Type: TEXT</P> */ - public static final String _SYNC_DIRTY = "_sync_dirty"; + public static final String CAL_SYNC9 = "cal_sync9"; + /** + * Generic column for use by sync adapters. Column name. + * <P>Type: TEXT</P> + */ + public static final String CAL_SYNC10 = "cal_sync10"; } /** - * Columns from the Account information used by Calendars and Events tables. + * Columns for Sync information used by Calendars and Events tables. These + * have specific uses which are expected to be consistent by the app and + * sync adapter. + * + * @hide */ - public interface AccountColumns { + public interface SyncColumns extends CalendarSyncColumns { /** - * The name of the account instance to which this row belongs, which when paired with - * {@link #ACCOUNT_TYPE} identifies a specific account. + * The account that was used to sync the entry to the device. If the + * account_type is not {@link #ACCOUNT_TYPE_LOCAL} then the name and + * type must match an account on the device or the calendar will be + * deleted. * <P>Type: TEXT</P> */ public static final String ACCOUNT_NAME = "account_name"; /** - * The type of account to which this row belongs, which when paired with - * {@link #ACCOUNT_NAME} identifies a specific account. + * The type of the account that was used to sync the entry to the + * device. A type of {@link #ACCOUNT_TYPE_LOCAL} will keep this event + * form being deleted if there are no matching accounts on the device. * <P>Type: TEXT</P> */ public static final String ACCOUNT_TYPE = "account_type"; + + /** + * The unique ID for a row assigned by the sync source. NULL if the row + * has never been synced. This is used as a reference id for exceptions + * along with {@link BaseColumns#_ID}. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ID = "_sync_id"; + + /** + * Used to indicate that local, unsynced, changes are present. + * <P>Type: INTEGER (long)</P> + */ + public static final String DIRTY = "dirty"; + + /** + * Whether the row has been deleted but not synced to the server. A + * deleted row should be ignored. + * <P> + * Type: INTEGER (boolean) + * </P> + */ + public static final String DELETED = "deleted"; + + /** + * If set to 1 this causes events on this calendar to be duplicated with + * {@link EventsColumns#LAST_SYNCED} set to 1 whenever the event + * transitions from non-dirty to dirty. The duplicated event will not be + * expanded in the instances table and will only show up in sync adapter + * queries of the events table. It will also be deleted when the + * originating event has its dirty flag cleared by the sync adapter. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String CAN_PARTIALLY_UPDATE = "canPartiallyUpdate"; } /** - * Columns from the Calendars table that other tables join into themselves. + * Columns specific to the Calendars Uri that other Uris can query. */ - public interface CalendarsColumns { + private interface CalendarsColumns { /** * The color of the calendar * <P>Type: INTEGER (color value)</P> */ - public static final String COLOR = "color"; + public static final String CALENDAR_COLOR = "calendar_color"; + + /** + * The display name of the calendar. Column name. + * <P>Type: TEXT</P> + */ + public static final String CALENDAR_DISPLAY_NAME = "calendar_displayName"; /** * The level of access that the user has for the calendar * <P>Type: INTEGER (one of the values below)</P> */ - public static final String ACCESS_LEVEL = "access_level"; + public static final String CALENDAR_ACCESS_LEVEL = "calendar_access_level"; /** Cannot access the calendar */ - public static final int NO_ACCESS = 0; + public static final int CAL_ACCESS_NONE = 0; /** Can only see free/busy information about the calendar */ - public static final int FREEBUSY_ACCESS = 100; + public static final int CAL_ACCESS_FREEBUSY = 100; /** Can read all event details */ - public static final int READ_ACCESS = 200; - public static final int RESPOND_ACCESS = 300; - public static final int OVERRIDE_ACCESS = 400; - /** Full access to modify the calendar, but not the access control settings */ - public static final int CONTRIBUTOR_ACCESS = 500; - public static final int EDITOR_ACCESS = 600; + public static final int CAL_ACCESS_READ = 200; + /** Can reply yes/no/maybe to an event */ + public static final int CAL_ACCESS_RESPOND = 300; + /** not used */ + public static final int CAL_ACCESS_OVERRIDE = 400; + /** Full access to modify the calendar, but not the access control + * settings + */ + public static final int CAL_ACCESS_CONTRIBUTOR = 500; + /** Full access to modify the calendar, but not the access control + * settings + */ + public static final int CAL_ACCESS_EDITOR = 600; /** Full access to the calendar */ - public static final int OWNER_ACCESS = 700; + public static final int CAL_ACCESS_OWNER = 700; /** Domain admin */ - public static final int ROOT_ACCESS = 800; + public static final int CAL_ACCESS_ROOT = 800; /** * Is the calendar selected to be displayed? + * 0 - do not show events associated with this calendar. + * 1 - show events associated with this calendar * <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 + * The time zone the calendar is associated with. * <P>Type: TEXT</P> */ - public static final String TIMEZONE = "timezone"; + public static final String CALENDAR_TIME_ZONE = "calendar_timezone"; /** - * If this calendar is in the list of calendars that are selected for - * syncing then "sync_events" is 1, otherwise 0. + * Is this calendar synced and are its events stored on the device? + * 0 - Do not sync this calendar or store events for this calendar. + * 1 - Sync down events for this calendar. * <p>Type: INTEGER (boolean)</p> */ public static final String SYNC_EVENTS = "sync_events"; /** - * Sync state data. - * <p>Type: String (blob)</p> + * The owner account for this calendar, based on the calendar feed. + * This will be different from the _SYNC_ACCOUNT for delegated calendars. + * Column name. + * <P>Type: String</P> */ - public static final String SYNC_STATE = "sync_state"; + public static final String OWNER_ACCOUNT = "ownerAccount"; /** - * Whether the row has been deleted. A deleted row should be ignored. + * Can the organizer respond to the event? If no, the status of the + * organizer should not be shown by the UI. Defaults to 1. Column name. * <P>Type: INTEGER (boolean)</P> */ - public static final String DELETED = "deleted"; + public static final String CAN_ORGANIZER_RESPOND = "canOrganizerRespond"; + + /** + * Can the organizer modify the time zone of the event? Column name. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String CAN_MODIFY_TIME_ZONE = "canModifyTimeZone"; + + /** + * The maximum number of reminders allowed for an event. Column name. + * <P>Type: INTEGER</P> + */ + public static final String MAX_REMINDERS = "maxReminders"; + + /** + * A comma separated list of reminder methods supported for this + * calendar in the format "#,#,#". Valid types are + * {@link Reminders#METHOD_DEFAULT}, {@link Reminders#METHOD_ALERT}, + * {@link Reminders#METHOD_EMAIL}, {@link Reminders#METHOD_SMS}. Column + * name. + * <P>Type: TEXT</P> + */ + public static final String ALLOWED_REMINDERS = "allowedReminders"; } /** * Class that represents a Calendar Entity. There is one entry per calendar. + * This is a helper class to make batch operations easier. */ public static class CalendarsEntity implements BaseColumns, SyncColumns, CalendarsColumns { + /** + * The default Uri used when creating a new calendar EntityIterator. + */ + @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/calendar_entities"); - public static EntityIterator newEntityIterator(Cursor cursor, ContentResolver resolver) { - return new EntityIteratorImpl(cursor, resolver); - } - - public static EntityIterator newEntityIterator(Cursor cursor, - ContentProviderClient provider) { - return new EntityIteratorImpl(cursor, provider); + /** + * Creates an entity iterator for the given cursor. It assumes the + * cursor contains a calendars query. + * + * @param cursor query on {@link #CONTENT_URI} + * @return an EntityIterator of calendars + */ + public static EntityIterator newEntityIterator(Cursor cursor) { + return new EntityIteratorImpl(cursor); } private static class EntityIteratorImpl extends CursorEntityIterator { - private final ContentResolver mResolver; - private final ContentProviderClient mProvider; - - public EntityIteratorImpl(Cursor cursor, ContentResolver resolver) { - super(cursor); - mResolver = resolver; - mProvider = null; - } - public EntityIteratorImpl(Cursor cursor, ContentProviderClient provider) { + public EntityIteratorImpl(Cursor cursor) { super(cursor); - mResolver = null; - mProvider = provider; } @Override @@ -282,35 +377,46 @@ public final class Calendar { ContentValues cv = new ContentValues(); cv.put(_ID, calendarId); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ACCOUNT); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ACCOUNT_TYPE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ACCOUNT_NAME); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ACCOUNT_TYPE); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_TIME); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA); - DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY); - DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_MARK); - - DatabaseUtils.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.cursorLongToContentValuesIfPresent(cursor, cv, DIRTY); + + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC1); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC2); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC3); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC4); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC5); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC6); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC7); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC8); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC9); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC10); 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); + Calendars.CALENDAR_DISPLAY_NAME); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, + Calendars.CALENDAR_COLOR); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, CALENDAR_ACCESS_LEVEL); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, VISIBLE); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SYNC_EVENTS); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.LOCATION); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, TIMEZONE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + Calendars.CALENDAR_LOCATION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CALENDAR_TIME_ZONE); 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, + Calendars.CAN_PARTIALLY_UPDATE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + Calendars.ALLOWED_REMINDERS); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED); @@ -327,26 +433,40 @@ public final class Calendar { } /** - * Contains a list of available calendars. + * Fields and helpers for interacting with Calendars. */ - public static class Calendars implements BaseColumns, SyncColumns, AccountColumns, - CalendarsColumns - { - private static final String WHERE_DELETE_FOR_ACCOUNT = Calendars._SYNC_ACCOUNT + "=?" - + " AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?"; + public static class Calendars implements BaseColumns, SyncColumns, CalendarsColumns { + private static final String WHERE_DELETE_FOR_ACCOUNT = Calendars.ACCOUNT_NAME + "=?" + + " AND " + + Calendars.ACCOUNT_TYPE + "=?"; - public static final Cursor query(ContentResolver cr, String[] projection, - String where, String orderBy) - { - return cr.query(CONTENT_URI, projection, where, - null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + /** + * Helper function for generating a calendars query. This is blocking + * and should not be used on the UI thread. See + * {@link ContentResolver#query(Uri, String[], String, String[], String)} + * for more details about using the parameters. + * + * @param cr The ContentResolver to query with + * @param projection A list of columns to return + * @param selection A formatted selection string + * @param selectionArgs arguments to the selection string + * @param orderBy How to order the returned rows + * @return + */ + public static final Cursor query(ContentResolver cr, String[] projection, String selection, + String[] selectionArgs, String orderBy) { + return cr.query(CONTENT_URI, projection, selection, selectionArgs, + orderBy == null ? DEFAULT_SORT_ORDER : orderBy); } /** - * Convenience method perform a delete on the Calendar provider + * Convenience method perform a delete on the Calendar provider. This is + * a blocking call and should not be used on the UI thread. * * @param cr the ContentResolver - * @param selection the rows to delete + * @param selection A filter to apply to rows before deleting, formatted + * as an SQL WHERE clause (excluding the WHERE itself). + * @param selectionArgs Fill in the '?'s in the selection * @return the count of rows that were deleted */ public static int delete(ContentResolver cr, String selection, String[] selectionArgs) @@ -356,10 +476,12 @@ public final class Calendar { /** * Convenience method to delete all calendars that match the account. + * This is a blocking call and should not be used on the UI thread. * * @param cr the ContentResolver - * @param account the account whose rows should be deleted - * @return the count of rows that were deleted + * @param account the account whose calendars and events should be + * deleted + * @return the count of calendar rows that were deleted */ public static int deleteCalendarsForAccount(ContentResolver cr, Account account) { // delete all calendars that match this account @@ -369,8 +491,9 @@ public final class Calendar { } /** - * The content:// style URL for this table + * The content:// style URL for accessing Calendars */ + @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/calendars"); /** @@ -379,88 +502,76 @@ public final class Calendar { public static final String DEFAULT_SORT_ORDER = "displayName"; /** - * The URL to the calendar - * <P>Type: TEXT (URL)</P> - */ - public static final String URL = "url"; - - /** - * The URL for the calendar itself - * <P>Type: TEXT (URL)</P> - */ - public static final String SELF_URL = "selfUrl"; - - /** - * The URL for the calendar to be edited - * <P>Type: TEXT (URL)</P> - */ - public static final String EDIT_URL = "editUrl"; - - /** - * The URL for the calendar events - * <P>Type: TEXT (URL)</P> - */ - public static final String EVENTS_URL = "eventsUrl"; - - /** - * The name of the calendar + * The name of the calendar. Column name. * <P>Type: TEXT</P> */ public static final String NAME = "name"; /** - * The display name of the calendar + * The default location for the calendar. Column name. * <P>Type: TEXT</P> */ - public static final String DISPLAY_NAME = "displayName"; - - /** - * The location the of the events in the calendar - * <P>Type: TEXT</P> - */ - public static final String LOCATION = "location"; - - /** - * The owner account for this calendar, based on the calendar feed. - * This will be different from the _SYNC_ACCOUNT for delegated calendars. - * <P>Type: String</P> - */ - public static final String OWNER_ACCOUNT = "ownerAccount"; - - /** - * Can the organizer respond to the event? If no, the status of the - * 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 CALENDAR_LOCATION = "calendar_location"; + + /** + * These fields are only writable by a sync adapter. To modify them the + * caller must include {@link #CALLER_IS_SYNCADAPTER}, + * {@link #ACCOUNT_NAME}, and {@link #ACCOUNT_TYPE} in the Uri's query + * parameters. + */ + public static final String[] SYNC_WRITABLE_COLUMNS = new String[] { + ACCOUNT_NAME, + ACCOUNT_TYPE, + _SYNC_ID, + DIRTY, + OWNER_ACCOUNT, + MAX_REMINDERS, + CAN_MODIFY_TIME_ZONE, + CAN_ORGANIZER_RESPOND, + CAN_PARTIALLY_UPDATE, + CALENDAR_LOCATION, + CALENDAR_TIME_ZONE, + CALENDAR_ACCESS_LEVEL, + DELETED, + CAL_SYNC1, + CAL_SYNC2, + CAL_SYNC3, + CAL_SYNC4, + CAL_SYNC5, + CAL_SYNC6, + CAL_SYNC7, + CAL_SYNC8, + CAL_SYNC9, + CAL_SYNC10, + }; } /** * Columns from the Attendees table that other tables join into themselves. */ - public interface AttendeesColumns { + private interface AttendeesColumns { /** - * The id of the event. + * The id of the event. Column name. * <P>Type: INTEGER</P> */ public static final String EVENT_ID = "event_id"; /** - * The name of the attendee. + * The name of the attendee. Column name. * <P>Type: STRING</P> */ public static final String ATTENDEE_NAME = "attendeeName"; /** - * The email address of the attendee. + * The email address of the attendee. Column name. * <P>Type: STRING</P> */ public static final String ATTENDEE_EMAIL = "attendeeEmail"; /** - * The relationship of the attendee to the user. - * <P>Type: INTEGER (one of {@link #RELATIONSHIP_ATTENDEE}, ...}. + * The relationship of the attendee to the user. Column name. + * <P>Type: INTEGER (one of {@link #RELATIONSHIP_ATTENDEE}, ...}.</P> */ public static final String ATTENDEE_RELATIONSHIP = "attendeeRelationship"; @@ -471,8 +582,8 @@ public final class Calendar { public static final int RELATIONSHIP_SPEAKER = 4; /** - * The type of attendee. - * <P>Type: Integer (one of {@link #TYPE_REQUIRED}, {@link #TYPE_OPTIONAL}) + * The type of attendee. Column name. + * <P>Type: Integer (one of {@link #TYPE_REQUIRED}, {@link #TYPE_OPTIONAL})</P> */ public static final String ATTENDEE_TYPE = "attendeeType"; @@ -481,8 +592,8 @@ public final class Calendar { public static final int TYPE_OPTIONAL = 2; /** - * The attendance status of the attendee. - * <P>Type: Integer (one of {@link #ATTENDEE_STATUS_ACCEPTED}, ...}. + * The attendance status of the attendee. Column name. + * <P>Type: Integer (one of {@link #ATTENDEE_STATUS_ACCEPTED}, ...).</P> */ public static final String ATTENDEE_STATUS = "attendeeStatus"; @@ -493,50 +604,77 @@ public final class Calendar { public static final int ATTENDEE_STATUS_TENTATIVE = 4; } + /** + * Fields and helpers for interacting with Attendees. + */ public static final class Attendees implements BaseColumns, AttendeesColumns, EventsColumns { + + /** + * The content:// style URL for accessing Attendees data + */ + @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/attendees"); + /** + * the projection used by the attendees query + */ + public static final String[] PROJECTION = new String[] { + _ID, ATTENDEE_NAME, ATTENDEE_EMAIL, ATTENDEE_RELATIONSHIP, ATTENDEE_STATUS,}; + private static final String ATTENDEES_WHERE = Attendees.EVENT_ID + "=?"; - // TODO: fill out this class when we actually start utilizing attendees - // in the calendar application. + /** + * Queries all attendees associated with the given event. This is a + * blocking call and should not be done on the UI thread. + * + * @param cr The content resolver to use for the query + * @param eventId The id of the event to retrieve attendees for + * @return A Cursor containing all attendees for the event + */ + public static final Cursor query(ContentResolver cr, long eventId) { + String[] attArgs = {Long.toString(eventId)}; + return cr.query(CONTENT_URI, PROJECTION, ATTENDEES_WHERE, attArgs /* selection args */, + null /* sort order */); + } } /** * Columns from the Events table that other tables join into themselves. */ - public interface EventsColumns { - /** - * The calendar the event belongs to - * <P>Type: INTEGER (foreign key to the Calendars table)</P> - */ - public static final String CALENDAR_ID = "calendar_id"; + private interface EventsColumns { /** - * The URI for an HTML version of this event. - * <P>Type: TEXT</P> + * The {@link Calendars#_ID} of the calendar the event belongs to. + * Column name. + * <P>Type: INTEGER</P> */ - public static final String HTML_URI = "htmlUri"; + public static final String CALENDAR_ID = "calendar_id"; /** - * The title of the event + * The title of the event. Column name. * <P>Type: TEXT</P> */ public static final String TITLE = "title"; /** - * The description of the event + * The description of the event. Column name. * <P>Type: TEXT</P> */ public static final String DESCRIPTION = "description"; /** - * Where the event takes place. + * Where the event takes place. Column name. * <P>Type: TEXT</P> */ public static final String EVENT_LOCATION = "eventLocation"; /** - * The event status - * <P>Type: INTEGER (int)</P> + * A secondary color for the individual event. Column name. + * <P>Type: INTEGER</P> + */ + public static final String EVENT_COLOR = "eventColor"; + + /** + * The event status. Column name. + * <P>Type: INTEGER (one of {@link #STATUS_TENTATIVE}...)</P> */ public static final String STATUS = "eventStatus"; @@ -548,153 +686,232 @@ public final class Calendar { * This is a copy of the attendee status for the owner of this event. * This field is copied here so that we can efficiently filter out * events that are declined without having to look in the Attendees - * table. + * table. Column name. * * <P>Type: INTEGER (int)</P> */ public static final String SELF_ATTENDEE_STATUS = "selfAttendeeStatus"; /** - * This column is available for use by sync adapters + * This column is available for use by sync adapters. Column name. * <P>Type: TEXT</P> */ - public static final String SYNC_ADAPTER_DATA = "syncAdapterData"; + public static final String SYNC_DATA1 = "sync_data1"; /** - * The comments feed uri. + * This column is available for use by sync adapters. Column name. * <P>Type: TEXT</P> */ - public static final String COMMENTS_URI = "commentsUri"; + public static final String SYNC_DATA2 = "sync_data2"; /** - * The time the event starts - * <P>Type: INTEGER (long; millis since epoch)</P> + * This column is available for use by sync adapters. Column name. + * <P>Type: TEXT</P> */ - public static final String DTSTART = "dtstart"; + public static final String SYNC_DATA3 = "sync_data3"; /** - * The time the event ends - * <P>Type: INTEGER (long; millis since epoch)</P> + * This column is available for use by sync adapters. Column name. + * <P>Type: TEXT</P> */ - public static final String DTEND = "dtend"; + public static final String SYNC_DATA4 = "sync_data4"; + + /** + * This column is available for use by sync adapters. Column name. + * <P>Type: TEXT</P> + */ + public static final String SYNC_DATA5 = "sync_data5"; + + /** + * This column is available for use by sync adapters. Column name. + * <P>Type: TEXT</P> + */ + public static final String SYNC_DATA6 = "sync_data6"; + + /** + * This column is available for use by sync adapters. Column name. + * <P>Type: TEXT</P> + */ + public static final String SYNC_DATA7 = "sync_data7"; + + /** + * This column is available for use by sync adapters. Column name. + * <P>Type: TEXT</P> + */ + public static final String SYNC_DATA8 = "sync_data8"; + + /** + * This column is available for use by sync adapters. Column name. + * <P>Type: TEXT</P> + */ + public static final String SYNC_DATA9 = "sync_data9"; + + /** + * This column is available for use by sync adapters. Column name. + * <P>Type: TEXT</P> + */ + public static final String SYNC_DATA10 = "sync_data10"; + + /** + * Used to indicate that a row is not a real event but an original copy of a locally + * modified event. A copy is made when an event changes from non-dirty to dirty and the + * event is on a calendar with {@link Calendars#CAN_PARTIALLY_UPDATE} set to 1. This copy + * does not get expanded in the instances table and is only visible in queries made by a + * sync adapter. The copy gets removed when the event is changed back to non-dirty by a + * sync adapter. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String LAST_SYNCED = "lastSynced"; /** - * The time the event starts with allDay events in a local tz + * The time the event starts in UTC millis since epoch. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ - public static final String DTSTART2 = "dtstart2"; + public static final String DTSTART = "dtstart"; /** - * The time the event ends with allDay events in a local tz + * The time the event ends in UTC millis since epoch. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ - public static final String DTEND2 = "dtend2"; + public static final String DTEND = "dtend"; /** - * The duration of the event + * The duration of the event in RFC2445 format. Column name. * <P>Type: TEXT (duration in RFC2445 format)</P> */ public static final String DURATION = "duration"; /** - * The timezone for the event. - * <P>Type: TEXT + * The timezone for the event. Column name. + * <P>Type: TEXT</P> */ public static final String EVENT_TIMEZONE = "eventTimezone"; /** - * The timezone for the event, allDay events will have a local tz instead of UTC - * <P>Type: TEXT + * The timezone for the end time of the event. Column name. + * <P>Type: TEXT</P> */ - public static final String EVENT_TIMEZONE2 = "eventTimezone2"; + public static final String EVENT_END_TIMEZONE = "eventEndTimezone"; /** - * Whether the event lasts all day or not + * Is the event all day (time zone independent). Column name. * <P>Type: INTEGER (boolean)</P> */ public static final String ALL_DAY = "allDay"; /** - * Visibility for the event. - * <P>Type: INTEGER</P> + * Defines how the event shows up for others when the calendar is + * shared. Column name. + * <P>Type: INTEGER (One of {@link #ACCESS_DEFAULT}, ...)</P> */ - public static final String VISIBILITY = "visibility"; - - public static final int VISIBILITY_DEFAULT = 0; - public static final int VISIBILITY_CONFIDENTIAL = 1; - public static final int VISIBILITY_PRIVATE = 2; - public static final int VISIBILITY_PUBLIC = 3; + public static final String ACCESS_LEVEL = "accessLevel"; /** - * Transparency for the event -- does the event consume time on the calendar? - * <P>Type: INTEGER</P> + * Default access is controlled by the server and will be treated as + * public on the device. */ - public static final String TRANSPARENCY = "transparency"; + public static final int ACCESS_DEFAULT = 0; + /** + * Confidential is not used by the app. + */ + public static final int ACCESS_CONFIDENTIAL = 1; + /** + * Private shares the event as a free/busy slot with no details. + */ + public static final int ACCESS_PRIVATE = 2; + /** + * Public makes the contents visible to anyone with access to the + * calendar. + */ + public static final int ACCESS_PUBLIC = 3; - public static final int TRANSPARENCY_OPAQUE = 0; + /** + * If this event counts as busy time or is still free time that can be + * scheduled over. Column name. + * <P>Type: INTEGER (One of {@link #AVAILABILITY_BUSY}, + * {@link #AVAILABILITY_FREE})</P> + */ + public static final String AVAILABILITY = "availability"; - public static final int TRANSPARENCY_TRANSPARENT = 1; + /** + * Indicates that this event takes up time and will conflict with other + * events. + */ + public static final int AVAILABILITY_BUSY = 0; + /** + * Indicates that this event is free time and will not conflict with + * other events. + */ + public static final int AVAILABILITY_FREE = 1; /** - * Whether the event has an alarm or not + * Whether the event has an alarm or not. Column name. * <P>Type: INTEGER (boolean)</P> */ public static final String HAS_ALARM = "hasAlarm"; /** - * Whether the event has extended properties or not + * Whether the event has extended properties or not. Column name. * <P>Type: INTEGER (boolean)</P> */ public static final String HAS_EXTENDED_PROPERTIES = "hasExtendedProperties"; /** - * The recurrence rule for the event. - * than one. + * The recurrence rule for the event. Column name. * <P>Type: TEXT</P> */ public static final String RRULE = "rrule"; /** - * The recurrence dates for the event. + * The recurrence dates for the event. Column name. * <P>Type: TEXT</P> */ public static final String RDATE = "rdate"; /** - * The recurrence exception rule for the event. + * The recurrence exception rule for the event. Column name. * <P>Type: TEXT</P> */ public static final String EXRULE = "exrule"; /** - * The recurrence exception dates for the event. + * The recurrence exception dates for the event. Column name. * <P>Type: TEXT</P> */ public static final String EXDATE = "exdate"; /** + * The {@link Events#_ID} of the original recurring event for which this + * event is an exception. Column name. + * <P>Type: TEXT</P> + */ + public static final String ORIGINAL_ID = "original_id"; + + /** * The _sync_id of the original recurring event for which this event is - * an exception. + * an exception. The provider should keep the original_id in sync when + * this is updated. Column name. * <P>Type: TEXT</P> */ - public static final String ORIGINAL_EVENT = "originalEvent"; + public static final String ORIGINAL_SYNC_ID = "original_sync_id"; /** * The original instance time of the recurring event for which this - * event is an exception. + * event is an exception. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String ORIGINAL_INSTANCE_TIME = "originalInstanceTime"; /** * The allDay status (true or false) of the original recurring event - * for which this event is an exception. + * for which this event is an exception. Column name. * <P>Type: INTEGER (boolean)</P> */ public static final String ORIGINAL_ALL_DAY = "originalAllDay"; /** - * The last date this event repeats on, or NULL if it never ends + * The last date this event repeats on, or NULL if it never ends. Column + * name. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String LAST_DATE = "lastDate"; @@ -702,73 +919,80 @@ public final class Calendar { /** * Whether the event has attendee information. True if the event * has full attendee data, false if the event has information about - * self only. + * self only. Column name. * <P>Type: INTEGER (boolean)</P> */ public static final String HAS_ATTENDEE_DATA = "hasAttendeeData"; /** - * Whether guests can modify the event. + * Whether guests can modify the event. Column name. * <P>Type: INTEGER (boolean)</P> */ public static final String GUESTS_CAN_MODIFY = "guestsCanModify"; /** - * Whether guests can invite other guests. + * Whether guests can invite other guests. Column name. * <P>Type: INTEGER (boolean)</P> */ public static final String GUESTS_CAN_INVITE_OTHERS = "guestsCanInviteOthers"; /** - * Whether guests can see the list of attendees. + * Whether guests can see the list of attendees. Column name. * <P>Type: INTEGER (boolean)</P> */ public static final String GUESTS_CAN_SEE_GUESTS = "guestsCanSeeGuests"; /** - * Email of the organizer (owner) of the event. + * Email of the organizer (owner) of the event. Column name. * <P>Type: STRING</P> */ public static final String ORGANIZER = "organizer"; /** - * Whether the user can invite others to the event. - * The GUESTS_CAN_INVITE_OTHERS is a setting that applies to an arbitrary guest, - * while CAN_INVITE_OTHERS indicates if the user can invite others (either through - * GUESTS_CAN_INVITE_OTHERS or because the user has modify access to the event). + * Whether the user can invite others to the event. The + * GUESTS_CAN_INVITE_OTHERS is a setting that applies to an arbitrary + * guest, while CAN_INVITE_OTHERS indicates if the user can invite + * others (either through GUESTS_CAN_INVITE_OTHERS or because the user + * has modify access to the event). Column name. * <P>Type: INTEGER (boolean, readonly)</P> */ public static final String CAN_INVITE_OTHERS = "canInviteOthers"; - - /** - * The owner account for this calendar, based on the calendar (foreign - * key into the calendars table). - * <P>Type: String</P> - */ - public static final String OWNER_ACCOUNT = "ownerAccount"; - - /** - * Whether the row has been deleted. A deleted row should be ignored. - * <P>Type: INTEGER (boolean)</P> - */ - public static final String DELETED = "deleted"; } /** - * Contains one entry per calendar event. Recurring events show up as a single entry. + * Class that represents an Event Entity. There is one entry per event. + * Recurring events show up as a single entry. This is a helper class to + * make batch operations easier. A {@link ContentResolver} or + * {@link ContentProviderClient} is required as the helper does additional + * queries to add reminders and attendees to each entry. */ - public static final class EventsEntity implements BaseColumns, SyncColumns, AccountColumns, - EventsColumns { + public static final class EventsEntity implements BaseColumns, SyncColumns, EventsColumns { /** * The content:// style URL for this table */ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/event_entities"); + /** + * Creates a new iterator for events + * + * @param cursor An event query + * @param resolver For performing additional queries + * @return an EntityIterator containing one entity per event in the + * cursor + */ public static EntityIterator newEntityIterator(Cursor cursor, ContentResolver resolver) { return new EntityIteratorImpl(cursor, resolver); } + /** + * Creates a new iterator for events + * + * @param cursor An event query + * @param provider For performing additional queries + * @return an EntityIterator containing one entity per event in the + * cursor + */ public static EntityIterator newEntityIterator(Cursor cursor, ContentProviderClient provider) { return new EntityIteratorImpl(cursor, provider); @@ -827,20 +1051,19 @@ public final class Calendar { ContentValues cv = new ContentValues(); cv.put(Events._ID, eventId); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, CALENDAR_ID); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HTML_URI); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, TITLE); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, DESCRIPTION); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_LOCATION); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, STATUS); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SELF_ATTENDEE_STATUS); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, COMMENTS_URI); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DTSTART); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DTEND); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, DURATION); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_TIMEZONE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_END_TIMEZONE); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ALL_DAY); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, VISIBILITY); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, TRANSPARENCY); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ACCESS_LEVEL); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, AVAILABILITY); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HAS_ALARM); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HAS_EXTENDED_PROPERTIES); @@ -848,7 +1071,8 @@ public final class Calendar { DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, RDATE); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EXRULE); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EXDATE); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORIGINAL_EVENT); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORIGINAL_SYNC_ID); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORIGINAL_ID); DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, ORIGINAL_INSTANCE_TIME); DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ORIGINAL_ALL_DAY); @@ -860,13 +1084,29 @@ public final class Calendar { DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, GUESTS_CAN_SEE_GUESTS); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORGANIZER); DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_DATA); - DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION); - DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, EventsColumns.DELETED); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.SYNC1); - DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, - Events.SYNC_ADAPTER_DATA); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DIRTY); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, LAST_SYNCED); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA1); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA2); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA3); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA4); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA5); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA6); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA7); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA8); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA9); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC_DATA10); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC1); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC2); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC3); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC4); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC5); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC6); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC7); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC8); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC9); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, CAL_SYNC10); Entity entity = new Entity(cv); Cursor subCursor; @@ -955,64 +1195,139 @@ public final class Calendar { } /** - * Contains one entry per calendar event. Recurring events show up as a single entry. + * Fields and helpers for interacting with Events. */ - public static final class Events implements BaseColumns, SyncColumns, AccountColumns, - EventsColumns { - - private static final String[] FETCH_ENTRY_COLUMNS = - new String[] { Events._SYNC_ACCOUNT, Events._SYNC_ID }; - - private static final String[] ATTENDEES_COLUMNS = - new String[] { AttendeesColumns.ATTENDEE_NAME, - AttendeesColumns.ATTENDEE_EMAIL, - AttendeesColumns.ATTENDEE_RELATIONSHIP, - AttendeesColumns.ATTENDEE_TYPE, - AttendeesColumns.ATTENDEE_STATUS }; + public static final class Events implements BaseColumns, SyncColumns, EventsColumns, + CalendarsColumns { + /** + * Queries all events with the given projection. This is a blocking call + * and should not be done on the UI thread. + * + * @param cr The content resolver to use for the query + * @param projection The columns to return + * @return A Cursor containing all events in the db + */ public static final Cursor query(ContentResolver cr, String[] projection) { return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); } - public static final Cursor query(ContentResolver cr, String[] projection, - String where, String orderBy) { - return cr.query(CONTENT_URI, projection, where, - null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); - } - - private static String extractValue(ICalendar.Component component, - String propertyName) { - ICalendar.Property property = - component.getFirstProperty(propertyName); - if (property != null) { - return property.getValue(); - } - return null; + /** + * Queries events using the given projection, selection filter, and + * ordering. This is a blocking call and should not be done on the UI + * thread. For selection and selectionArgs usage see + * {@link ContentResolver#query(Uri, String[], String, String[], String)} + * + * @param cr The content resolver to use for the query + * @param projection The columns to return + * @param selection Filter on the query as an SQL WHERE statement + * @param selectionArgs Args to replace any '?'s in the selection + * @param orderBy How to order the rows as an SQL ORDER BY statement + * @return A Cursor containing the matching events + */ + public static final Cursor query(ContentResolver cr, String[] projection, String selection, + String[] selectionArgs, String orderBy) { + return cr.query(CONTENT_URI, projection, selection, null, + orderBy == null ? DEFAULT_SORT_ORDER : orderBy); } /** - * The content:// style URL for this table + * The content:// style URL for interacting with events. Appending an + * event id using {@link ContentUris#withAppendedId(Uri, long)} will + * specify a single event. */ + @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/events"); - public static final Uri DELETED_CONTENT_URI = - Uri.parse("content://" + AUTHORITY + "/deleted_events"); + /** + * The content:// style URI for recurring event exceptions. Insertions require an + * appended event ID. Deletion of exceptions requires both the original event ID and + * the exception event ID (see {@link Uri.Builder#appendPath}). + */ + public static final Uri EXCEPTION_CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/exception"); /** * The default sort order for this table */ - public static final String DEFAULT_SORT_ORDER = ""; + private static final String DEFAULT_SORT_ORDER = ""; + + /** + * These are columns that should only ever be updated by the provider, + * either because they are views mapped to another table or because they + * are used for provider only functionality. + */ + public static String[] PROVIDER_WRITABLE_COLUMNS = new String[] { + ACCOUNT_NAME, + ACCOUNT_TYPE, + CAL_SYNC1, + CAL_SYNC2, + CAL_SYNC3, + CAL_SYNC4, + CAL_SYNC5, + CAL_SYNC6, + CAL_SYNC7, + CAL_SYNC8, + CAL_SYNC9, + CAL_SYNC10, + ALLOWED_REMINDERS, + CALENDAR_ACCESS_LEVEL, + CALENDAR_COLOR, + CALENDAR_TIME_ZONE, + CAN_MODIFY_TIME_ZONE, + CAN_ORGANIZER_RESPOND, + CALENDAR_DISPLAY_NAME, + CAN_PARTIALLY_UPDATE, + SYNC_EVENTS, + VISIBLE, + }; + + /** + * These fields are only writable by a sync adapter. To modify them the + * caller must include CALLER_IS_SYNCADAPTER, _SYNC_ACCOUNT, and + * _SYNC_ACCOUNT_TYPE in the query parameters. + */ + public static final String[] SYNC_WRITABLE_COLUMNS = new String[] { + _SYNC_ID, + DIRTY, + SYNC_DATA1, + SYNC_DATA2, + SYNC_DATA3, + SYNC_DATA4, + SYNC_DATA5, + SYNC_DATA6, + SYNC_DATA7, + SYNC_DATA8, + SYNC_DATA9, + SYNC_DATA10, + }; } /** - * Contains one entry per calendar event instance. Recurring events show up every time - * they occur. + * Fields and helpers for interacting with Instances. An instance is a + * single occurrence of an event including time zone specific start and end + * days and minutes. */ 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"; + /** + * Performs a query to return all visible instances in the given range. + * This is a blocking function and should not be done on the UI thread. + * This will cause an expansion of recurring events to fill this time + * range if they are not already expanded and will slow down for larger + * time ranges with many recurring events. + * + * @param cr The ContentResolver to use for the query + * @param projection The columns to return + * @param begin The start of the time range to query in UTC millis since + * epoch + * @param end The end of the time range to query in UTC millis since + * epoch + * @return A Cursor containing all instances in the given range + */ public static final Cursor query(ContentResolver cr, String[] projection, long begin, long end) { Uri.Builder builder = CONTENT_URI.buildUpon(); @@ -1022,111 +1337,184 @@ public final class Calendar { null, DEFAULT_SORT_ORDER); } + /** + * Performs a query to return all visible instances in the given range + * that match the given query. This is a blocking function and should + * not be done on the UI thread. This will cause an expansion of + * recurring events to fill this time range if they are not already + * expanded and will slow down for larger time ranges with many + * recurring events. + * + * @param cr The ContentResolver to use for the query + * @param projection The columns to return + * @param begin The start of the time range to query in UTC millis since + * epoch + * @param end The end of the time range to query in UTC millis since + * epoch + * @param searchQuery A string of space separated search terms. Segments + * enclosed by double quotes will be treated as a single + * term. + * @return A Cursor of instances matching the search terms in the given + * time range + */ public static final Cursor query(ContentResolver cr, String[] projection, long begin, long end, String searchQuery) { Uri.Builder builder = CONTENT_SEARCH_URI.buildUpon(); ContentUris.appendId(builder, begin); ContentUris.appendId(builder, end); - return cr.query(builder.build(), projection, WHERE_CALENDARS_SELECTED, - new String[] { searchQuery }, DEFAULT_SORT_ORDER); + builder = builder.appendPath(searchQuery); + return cr.query(builder.build(), projection, WHERE_CALENDARS_SELECTED, null, + DEFAULT_SORT_ORDER); } - public static final Cursor query(ContentResolver cr, String[] projection, - long begin, long end, String where, String orderBy) { + /** + * Performs a query to return all visible instances in the given range + * that match the given selection. This is a blocking function and + * should not be done on the UI thread. This will cause an expansion of + * recurring events to fill this time range if they are not already + * expanded and will slow down for larger time ranges with many + * recurring events. + * + * @param cr The ContentResolver to use for the query + * @param projection The columns to return + * @param begin The start of the time range to query in UTC millis since + * epoch + * @param end The end of the time range to query in UTC millis since + * epoch + * @param selection Filter on the query as an SQL WHERE statement + * @param selectionArgs Args to replace any '?'s in the selection + * @param orderBy How to order the rows as an SQL ORDER BY statement + * @return A Cursor of instances matching the selection + */ + public static final Cursor query(ContentResolver cr, String[] projection, long begin, + long end, String selection, String[] selectionArgs, String orderBy) { Uri.Builder builder = CONTENT_URI.buildUpon(); ContentUris.appendId(builder, begin); ContentUris.appendId(builder, end); - if (TextUtils.isEmpty(where)) { - where = WHERE_CALENDARS_SELECTED; + if (TextUtils.isEmpty(selection)) { + selection = WHERE_CALENDARS_SELECTED; } else { - where = "(" + where + ") AND " + WHERE_CALENDARS_SELECTED; + selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED; } - return cr.query(builder.build(), projection, where, - null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + return cr.query(builder.build(), projection, selection, selectionArgs, + orderBy == null ? DEFAULT_SORT_ORDER : orderBy); } + /** + * Performs a query to return all visible instances in the given range + * that match the given selection. This is a blocking function and + * should not be done on the UI thread. This will cause an expansion of + * recurring events to fill this time range if they are not already + * expanded and will slow down for larger time ranges with many + * recurring events. + * + * @param cr The ContentResolver to use for the query + * @param projection The columns to return + * @param begin The start of the time range to query in UTC millis since + * epoch + * @param end The end of the time range to query in UTC millis since + * epoch + * @param searchQuery A string of space separated search terms. Segments + * enclosed by double quotes will be treated as a single + * term. + * @param selection Filter on the query as an SQL WHERE statement + * @param selectionArgs Args to replace any '?'s in the selection + * @param orderBy How to order the rows as an SQL ORDER BY statement + * @return A Cursor of instances matching the selection + */ public static final Cursor query(ContentResolver cr, String[] projection, long begin, - long end, String searchQuery, String where, String orderBy) { + long end, String searchQuery, String selection, String[] selectionArgs, + String orderBy) { Uri.Builder builder = CONTENT_SEARCH_URI.buildUpon(); ContentUris.appendId(builder, begin); ContentUris.appendId(builder, end); builder = builder.appendPath(searchQuery); - if (TextUtils.isEmpty(where)) { - where = WHERE_CALENDARS_SELECTED; + if (TextUtils.isEmpty(selection)) { + selection = WHERE_CALENDARS_SELECTED; } else { - where = "(" + where + ") AND " + WHERE_CALENDARS_SELECTED; + selection = "(" + selection + ") AND " + WHERE_CALENDARS_SELECTED; } - return cr.query(builder.build(), projection, where, null, + return cr.query(builder.build(), projection, selection, selectionArgs, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); } /** - * The content:// style URL for this table + * The content:// style URL for querying an instance range. The begin + * and end of the range to query should be added as path segments if + * this is used directly. */ + @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/instances/when"); + /** + * The content:// style URL for querying an instance range by Julian + * Day. The start and end day should be added as path segments if this + * is used directly. + */ public static final Uri CONTENT_BY_DAY_URI = Uri.parse("content://" + AUTHORITY + "/instances/whenbyday"); + /** + * The content:// style URL for querying an instance range with a search + * term. The begin, end, and search string should be appended as path + * segments if this is used directly. + */ public static final Uri CONTENT_SEARCH_URI = Uri.parse("content://" + AUTHORITY + "/instances/search"); + /** + * The content:// style URL for querying an instance range with a search + * term. The start day, end day, and search string should be appended as + * path segments if this is used directly. + */ public static final Uri CONTENT_SEARCH_BY_DAY_URI = Uri.parse("content://" + AUTHORITY + "/instances/searchbyday"); /** * The default sort order for this table. */ - public static final String DEFAULT_SORT_ORDER = "begin ASC"; + private static final String DEFAULT_SORT_ORDER = "begin ASC"; /** - * The sort order is: events with an earlier start time occur - * first and if the start times are the same, then events with - * a later end time occur first. The later end time is ordered - * first so that long-running events in the calendar views appear - * first. If the start and end times of two events are - * the same then we sort alphabetically on the title. This isn't - * required for correctness, it just adds a nice touch. - */ - public static final String SORT_CALENDAR_VIEW = "begin ASC, end DESC, title ASC"; - /** - * The beginning time of the instance, in UTC milliseconds + * The beginning time of the instance, in UTC milliseconds. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String BEGIN = "begin"; /** - * The ending time of the instance, in UTC milliseconds + * The ending time of the instance, in UTC milliseconds. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String END = "end"; /** - * The event for this instance + * The _id of the event for this instance. Column name. * <P>Type: INTEGER (long, foreign key to the Events table)</P> */ public static final String EVENT_ID = "event_id"; /** - * The Julian start day of the instance, relative to the local timezone + * The Julian start day of the instance, relative to the local time + * zone. Column name. * <P>Type: INTEGER (int)</P> */ public static final String START_DAY = "startDay"; /** - * The Julian end day of the instance, relative to the local timezone + * The Julian end day of the instance, relative to the local time + * zone. Column name. * <P>Type: INTEGER (int)</P> */ public static final String END_DAY = "endDay"; /** * The start minute of the instance measured from midnight in the - * local timezone. + * local time zone. Column name. * <P>Type: INTEGER (int)</P> */ public static final String START_MINUTE = "startMinute"; /** * The end minute of the instance measured from midnight in the - * local timezone. + * local time zone. Column name. * <P>Type: INTEGER (int)</P> */ public static final String END_MINUTE = "endMinute"; @@ -1134,14 +1522,12 @@ public final class Calendar { /** * CalendarCache stores some settings for calendar including the current - * time zone for the app. These settings are stored using a key/value + * time zone for the instaces. These settings are stored using a key/value * scheme. */ - public interface CalendarCacheColumns { + private interface CalendarCacheColumns { /** - * The key for the setting. Keys are defined in CalendarChache in the - * Calendar provider. - * TODO Add keys to this file + * The key for the setting. Keys are defined in {@link CalendarCache}. */ public static final String KEY = "key"; @@ -1211,7 +1597,7 @@ public final class Calendar { * the Instances table and these are all stored in the first (and only) * row of the CalendarMetaData table. */ - public interface CalendarMetaDataColumns { + private interface CalendarMetaDataColumns { /** * The local timezone that was used for precomputing the fields * in the Instances table. @@ -1245,34 +1631,51 @@ public final class Calendar { public static final String MAX_EVENTDAYS = "maxEventDays"; } + /** + * @hide + */ public static final class CalendarMetaData implements CalendarMetaDataColumns, BaseColumns { } - public interface EventDaysColumns { + private interface EventDaysColumns { /** - * The Julian starting day number. + * The Julian starting day number. Column name. * <P>Type: INTEGER (int)</P> */ public static final String STARTDAY = "startDay"; + /** + * The Julian ending day number. Column name. + * <P>Type: INTEGER (int)</P> + */ public static final String ENDDAY = "endDay"; } + /** + * Fields and helpers for querying for a list of days that contain events. + */ public static final class EventDays implements EventDaysColumns { - public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + - "/instances/groupbyday"); + private static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + + "/instances/groupbyday"); + /** + * The projection used by the EventDays query. + */ public static final String[] PROJECTION = { STARTDAY, ENDDAY }; - public static final String SELECTION = "selected=1"; + private static final String SELECTION = "selected=1"; /** - * Retrieves the days with events for the Julian days starting at "startDay" - * for "numDays". + * Retrieves the days with events for the Julian days starting at + * "startDay" for "numDays". It returns a cursor containing startday and + * endday representing the max range of days for all events beginning on + * each startday.This is a blocking function and should not be done on + * the UI thread. * * @param cr the ContentResolver * @param startDay the first Julian day in the range * @param numDays the number of days to load (must be at least 1) - * @return a database cursor + * @return a database cursor containing a list of start and end days for + * events */ public static final Cursor query(ContentResolver cr, int startDay, int numDays) { if (numDays < 1) { @@ -1287,9 +1690,9 @@ public final class Calendar { } } - public interface RemindersColumns { + private interface RemindersColumns { /** - * The event the reminder belongs to + * The event the reminder belongs to. Column name. * <P>Type: INTEGER (foreign key to the Events table)</P> */ public static final String EVENT_ID = "event_id"; @@ -1297,17 +1700,24 @@ public final class Calendar { /** * The minutes prior to the event that the alarm should ring. -1 * specifies that we should use the default value for the system. + * Column name. * <P>Type: INTEGER</P> */ public static final String MINUTES = "minutes"; + /** + * Passing this as a minutes value will use the default reminder + * minutes. + */ public static final int MINUTES_DEFAULT = -1; /** - * The alarm method, as set on the server. DEFAULT, ALERT, EMAIL, and - * SMS are possible values; the device will only process DEFAULT and - * ALERT reminders (the other types are simply stored so we can send the - * same reminder info back to the server when we make changes). + * The alarm method, as set on the server. {@link #METHOD_DEFAULT}, + * {@link #METHOD_ALERT}, {@link #METHOD_EMAIL}, and {@link #METHOD_SMS} + * are possible values; the device will only process + * {@link #METHOD_DEFAULT} and {@link #METHOD_ALERT} reminders (the + * other types are simply stored so we can send the same reminder info + * back to the server when we make changes). */ public static final String METHOD = "method"; @@ -1317,61 +1727,85 @@ public final class Calendar { public static final int METHOD_SMS = 3; } + /** + * Fields and helpers for accessing reminders for an event. + */ public static final class Reminders implements BaseColumns, RemindersColumns, EventsColumns { - public static final String TABLE_NAME = "Reminders"; + private static final String REMINDERS_WHERE = Calendar.Reminders.EVENT_ID + "=?"; + /** + * The projection used by the reminders query. + */ + public static final String[] PROJECTION = new String[] { + _ID, MINUTES, METHOD,}; + @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/reminders"); + + /** + * Queries all reminders associated with the given event. This is a + * blocking call and should not be done on the UI thread. + * + * @param cr The content resolver to use for the query + * @param eventId The id of the event to retrieve reminders for + * @return A Cursor containing all reminders for the event + */ + public static final Cursor query(ContentResolver cr, long eventId) { + String[] remArgs = {Long.toString(eventId)}; + return cr.query(CONTENT_URI, PROJECTION, REMINDERS_WHERE, remArgs /* selection args */, + null /* sort order */); + } } - public interface CalendarAlertsColumns { + private interface CalendarAlertsColumns { /** - * The event that the alert belongs to + * The event that the alert belongs to. Column name. * <P>Type: INTEGER (foreign key to the Events table)</P> */ public static final String EVENT_ID = "event_id"; /** - * The start time of the event, in UTC + * The start time of the event, in UTC. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String BEGIN = "begin"; /** - * The end time of the event, in UTC + * The end time of the event, in UTC. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String END = "end"; /** - * The alarm time of the event, in UTC + * The alarm time of the event, in UTC. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String ALARM_TIME = "alarmTime"; /** * The creation time of this database entry, in UTC. - * (Useful for debugging missed reminders.) + * Useful for debugging missed reminders. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String CREATION_TIME = "creationTime"; /** * The time that the alarm broadcast was received by the Calendar app, - * in UTC. (Useful for debugging missed reminders.) + * in UTC. Useful for debugging missed reminders. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String RECEIVED_TIME = "receivedTime"; /** * The time that the notification was created by the Calendar app, - * in UTC. (Useful for debugging missed reminders.) + * in UTC. Useful for debugging missed reminders. Column name. * <P>Type: INTEGER (long; millis since epoch)</P> */ public static final String NOTIFY_TIME = "notifyTime"; /** - * The state of this alert. It starts out as SCHEDULED, then when - * the alarm goes off, it changes to FIRED, and then when the user - * dismisses the alarm it changes to DISMISSED. + * The state of this alert. It starts out as {@link #SCHEDULED}, then + * when the alarm goes off, it changes to {@link #FIRED}, and then when + * the user dismisses the alarm it changes to {@link #DISMISSED}. Column + * name. * <P>Type: INTEGER</P> */ public static final String STATE = "state"; @@ -1381,21 +1815,33 @@ public final class Calendar { public static final int DISMISSED = 2; /** - * The number of minutes that this alarm precedes the start time - * <P>Type: INTEGER </P> + * The number of minutes that this alarm precedes the start time. Column + * name. + * <P>Type: INTEGER</P> */ public static final String MINUTES = "minutes"; /** - * The default sort order for this table + * The default sort order for this alerts queries */ public static final String DEFAULT_SORT_ORDER = "begin ASC,title ASC"; } + /** + * Fields and helpers for accessing calendar alerts information. These + * fields are for tracking which alerts have been fired. + */ public static final class CalendarAlerts implements BaseColumns, CalendarAlertsColumns, EventsColumns, CalendarsColumns { + /** + * @hide + */ public static final String TABLE_NAME = "CalendarAlerts"; + /** + * The Uri for querying calendar alert information + */ + @SuppressWarnings("hiding") public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/calendar_alerts"); @@ -1422,6 +1868,11 @@ public final class Calendar { private static final boolean DEBUG = true; + /** + * Helper for inserting an alarm time associated with an event + * + * @hide + */ public static final Uri insert(ContentResolver cr, long eventId, long begin, long end, long alarmTime, int minutes) { ContentValues values = new ContentValues(); @@ -1438,6 +1889,19 @@ public final class Calendar { return cr.insert(CONTENT_URI, values); } + /** + * Queries alerts info using the given projection, selection filter, and + * ordering. This is a blocking call and should not be done on the UI + * thread. For selection and selectionArgs usage see + * {@link ContentResolver#query(Uri, String[], String, String[], String)} + * + * @param cr The content resolver to use for the query + * @param projection The columns to return + * @param selection Filter on the query as an SQL WHERE statement + * @param selectionArgs Args to replace any '?'s in the selection + * @param sortOrder How to order the rows as an SQL ORDER BY statement + * @return A Cursor containing the matching alerts + */ public static final Cursor query(ContentResolver cr, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return cr.query(CONTENT_URI, projection, selection, selectionArgs, @@ -1446,12 +1910,13 @@ public final class Calendar { /** * Finds the next alarm after (or equal to) the given time and returns - * the time of that alarm or -1 if no such alarm exists. + * the time of that alarm or -1 if no such alarm exists. This is a + * blocking call and should not be done on the UI thread. * * @param cr the ContentResolver * @param millis the time in UTC milliseconds * @return the next alarm time greater than or equal to "millis", or -1 - * if no such alarm exists. + * if no such alarm exists. */ public static final long findNextAlarmTime(ContentResolver cr, long millis) { String selection = ALARM_TIME + ">=" + millis; @@ -1534,6 +1999,17 @@ public final class Calendar { } } + /** + * Schedules an alarm intent with the system AlarmManager that will + * cause the Calendar provider to recheck alarms. This is used to wake + * the Calendar alarm handler when an alarm is expected or to do a + * periodic refresh of alarm data. + * + * @param context A context for referencing system resources + * @param manager The AlarmManager to use or null + * @param alarmTime The time to fire the intent in UTC millis since + * epoch + */ public static void scheduleAlarm(Context context, AlarmManager manager, long alarmTime) { if (DEBUG) { Time time = new Time(); @@ -1593,27 +2069,32 @@ public final class Calendar { } } - public interface ExtendedPropertiesColumns { + private interface ExtendedPropertiesColumns { /** - * The event the extended property belongs to + * The event the extended property belongs to. Column name. * <P>Type: INTEGER (foreign key to the Events table)</P> */ public static final String EVENT_ID = "event_id"; /** * The name of the extended property. This is a uri of the form - * {scheme}#{local-name} convention. + * {scheme}#{local-name} convention. Column name. * <P>Type: TEXT</P> */ public static final String NAME = "name"; /** - * The value of the extended property. + * The value of the extended property. Column name. * <P>Type: TEXT</P> */ public static final String VALUE = "value"; } + /** + * Fields for accessing the Extended Properties. This is a generic set of + * name/value pairs for use by sync adapters or apps to add extra + * information to events. + */ public static final class ExtendedProperties implements BaseColumns, ExtendedPropertiesColumns, EventsColumns { public static final Uri CONTENT_URI = @@ -1634,7 +2115,7 @@ public final class Calendar { */ private SyncState() {} - public static final String CONTENT_DIRECTORY = + private static final String CONTENT_DIRECTORY = SyncStateContract.Constants.CONTENT_DIRECTORY; /** @@ -1647,39 +2128,43 @@ public final class Calendar { /** * Columns from the EventsRawTimes table */ - public interface EventsRawTimesColumns { + private interface EventsRawTimesColumns { /** - * The corresponding event id + * The corresponding event id. Column name. * <P>Type: INTEGER (long)</P> */ public static final String EVENT_ID = "event_id"; /** - * The RFC2445 compliant time the event starts + * The RFC2445 compliant time the event starts. Column name. * <P>Type: TEXT</P> */ public static final String DTSTART_2445 = "dtstart2445"; /** - * The RFC2445 compliant time the event ends + * The RFC2445 compliant time the event ends. Column name. * <P>Type: TEXT</P> */ public static final String DTEND_2445 = "dtend2445"; /** - * The RFC2445 compliant original instance time of the recurring event for which this - * event is an exception. + * The RFC2445 compliant original instance time of the recurring event + * for which this event is an exception. Column name. * <P>Type: TEXT</P> */ public static final String ORIGINAL_INSTANCE_TIME_2445 = "originalInstanceTime2445"; /** - * The RFC2445 compliant last date this event repeats on, or NULL if it never ends + * The RFC2445 compliant last date this event repeats on, or NULL if it + * never ends. Column name. * <P>Type: TEXT</P> */ public static final String LAST_DATE_2445 = "lastDate2445"; } + /** + * @hide + */ public static final class EventsRawTimes implements BaseColumns, EventsRawTimesColumns { } } diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index bf051f5..02faf49 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -77,9 +77,17 @@ public class CallLog { */ public static final String TYPE = "type"; + /** Call log type for incoming calls. */ public static final int INCOMING_TYPE = 1; + /** Call log type for outgoing calls. */ public static final int OUTGOING_TYPE = 2; + /** Call log type for missed calls. */ public static final int MISSED_TYPE = 3; + /** + * Call log type for voicemails. + * @hide + */ + public static final int VOICEMAIL_TYPE = 4; /** * The phone number as the user entered it. @@ -143,6 +151,13 @@ public class CallLog { public static final String CACHED_NUMBER_LABEL = "numberlabel"; /** + * URI of the voicemail entry. Populated only for {@link #VOICEMAIL_TYPE}. + * <P>Type: TEXT</P> + * @hide + */ + public static final String VOICEMAIL_URI = "voicemail_uri"; + + /** * Adds a call to the call log. * * @param ci the CallerInfo object to get the target contact from. Can be null diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 4f88612..6c14119 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -122,6 +122,17 @@ public final class ContactsContract { public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; /** + * An optional URI parameter for selection queries that instructs the + * provider to include the user's personal profile contact entry (if any) + * in the contact results. If present, the user's profile will always be + * the first entry returned. The default value is false. + * + * Specifying this parameter will result in a security error if the calling + * application does not have android.permission.READ_PROFILE permission. + */ + public static final String INCLUDE_PROFILE = "include_profile"; + + /** * A query parameter key used to specify the package that is requesting a query. * This is used for restricting data based on package name. * @@ -144,6 +155,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 { @@ -742,12 +774,18 @@ public final class ContactsContract { public static final String PHOTO_THUMBNAIL_URI = "photo_thumb_uri"; /** - * Lookup value that reflects the {@link Groups#GROUP_VISIBLE} state of - * any {@link CommonDataKinds.GroupMembership} for this contact. + * Flag that reflects the {@link Groups#GROUP_VISIBLE} state of any + * {@link CommonDataKinds.GroupMembership} for this contact. */ public static final String IN_VISIBLE_GROUP = "in_visible_group"; /** + * Flag that reflects whether this contact represents the user's + * personal profile entry. + */ + public static final String IS_USER_PROFILE = "is_user_profile"; + + /** * An indicator of whether this contact has at least one phone number. "1" if there is * at least one phone number, "0" otherwise. * <P>Type: INTEGER</P> @@ -1264,7 +1302,7 @@ public final class ContactsContract { * Base {@link Uri} for referencing multiple {@link Contacts} entry, * created by appending {@link #LOOKUP_KEY} using * {@link Uri#withAppendedPath(Uri, String)}. The lookup keys have to be - * encoded and joined with the colon (":") seperator. The resulting string + * encoded and joined with the colon (":") separator. The resulting string * has to be encoded again. Provides * {@link OpenableColumns} columns when queried, or returns the * referenced contact formatted as a vCard when opened through @@ -1717,6 +1755,88 @@ public final class ContactsContract { } } + /** + * <p> + * Constants for the user's profile data, which is represented as a single contact on + * the device that represents the user. The profile contact is not aggregated + * together automatically in the same way that normal contacts are; instead, each + * account on the device may contribute a single raw contact representing the user's + * personal profile data from that source. + * </p> + * <p> + * Access to the profile entry through these URIs (or incidental access to parts of + * the profile if retrieved directly via ID) requires additional permissions beyond + * the read/write contact permissions required by the provider. Querying for profile + * data requires android.permission.READ_PROFILE permission, and inserting or + * updating profile data requires android.permission.WRITE_PROFILE permission. + * </p> + * <h3>Operations</h3> + * <dl> + * <dt><b>Insert</b></dt> + * <dd>The user's profile entry cannot be created explicitly (attempting to do so + * will throw an exception). When a raw contact is inserted into the profile, the + * provider will check for the existence of a profile on the device. If one is + * found, the raw contact's {@link RawContacts#CONTACT_ID} column gets the _ID of + * the profile Contact. If no match is found, the profile Contact is created and + * its _ID is put into the {@link RawContacts#CONTACT_ID} column of the newly + * inserted raw contact.</dd> + * <dt><b>Update</b></dt> + * <dd>The profile Contact has the same update restrictions as Contacts in general, + * but requires the android.permission.WRITE_PROFILE permission.</dd> + * <dt><b>Delete</b></dt> + * <dd>The profile Contact cannot be explicitly deleted. It will be removed + * automatically if all of its constituent raw contact entries are deleted.</dd> + * <dt><b>Query</b></dt> + * <dd> + * <ul> + * <li>The {@link #CONTENT_URI} for profiles behaves in much the same way as + * retrieving a contact by ID, except that it will only ever return the user's + * profile contact. + * </li> + * <li> + * The profile contact supports all of the same sub-paths as an individual contact + * does - the content of the profile contact can be retrieved as entities or + * data rows. Similarly, specific raw contact entries can be retrieved by appending + * the desired raw contact ID within the profile. + * </li> + * </ul> + * </dd> + * </dl> + */ + public static final class Profile implements BaseColumns, ContactsColumns, + ContactOptionsColumns, ContactNameColumns, ContactStatusColumns { + /** + * This utility class cannot be instantiated + */ + private Profile() { + } + + /** + * The content:// style URI for this table, which requests the contact entry + * representing the user's personal profile data. + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "profile"); + + /** + * {@link Uri} for referencing the user's profile {@link Contacts} entry, + * Provides {@link OpenableColumns} columns when queried, or returns the + * user's profile contact formatted as a vCard when opened through + * {@link ContentResolver#openAssetFileDescriptor(Uri, String)}. + */ + public static final Uri CONTENT_VCARD_URI = Uri.withAppendedPath(CONTENT_URI, + "as_vcard"); + + /** + * {@link Uri} for referencing the raw contacts that make up the user's profile + * {@link Contacts} entry. An individual raw contact entry within the profile + * can be addressed by appending the raw contact ID. The entities or data within + * that specific raw contact can be requested by appending the entity or data + * path as well. + */ + public static final Uri CONTENT_RAW_CONTACTS_URI = Uri.withAppendedPath(CONTENT_URI, + "raw_contacts"); + } + protected interface RawContactsColumns { /** * A reference to the {@link ContactsContract.Contacts#_ID} that this @@ -1785,6 +1905,12 @@ public final class ContactsContract { * <P>Type: INTEGER</P> */ public static final String RAW_CONTACT_IS_READ_ONLY = "raw_contact_is_read_only"; + + /** + * Flag that reflects whether this raw contact belongs to the user's + * personal profile entry. + */ + public static final String RAW_CONTACT_IS_USER_PROFILE = "raw_contact_is_user_profile"; } /** @@ -6135,6 +6261,95 @@ public final class ContactsContract { } /** + * <p> + * API allowing applications to send usage information for each {@link Data} row to the + * Contacts Provider. + * </p> + * <p> + * With the feedback, Contacts Provider may return more contextually appropriate results for + * Data listing, typically supplied with + * {@link ContactsContract.Contacts#CONTENT_FILTER_URI}, + * {@link ContactsContract.CommonDataKinds.Email#CONTENT_FILTER_URI}, + * {@link ContactsContract.CommonDataKinds.Phone#CONTENT_FILTER_URI}, and users can benefit + * from better ranked (sorted) lists in applications that show auto-complete list. + * </p> + * <p> + * There is no guarantee for how this feedback is used, or even whether it is used at all. + * The ranking algorithm will make best efforts to use the feedback data, but the exact + * implementation, the storage data structures as well as the resulting sort order is device + * and version specific and can change over time. + * </p> + * <p> + * When updating usage information, users of this API need to use + * {@link ContentResolver#update(Uri, ContentValues, String, String[])} with a Uri constructed + * from {@link DataUsageFeedback#FEEDBACK_URI}. The Uri must contain one or more data id(s) as + * its last path. They also need to append a query parameter to the Uri, to specify the type of + * the communication, which enables the Contacts Provider to differentiate between kinds of + * interactions using the same contact data field (for example a phone number can be used to + * make phone calls or send SMS). + * </p> + * <p> + * Selection and selectionArgs are ignored and must be set to null. To get data ids, + * you may need to call {@link ContentResolver#query(Uri, String[], String, String[], String)} + * toward {@link Data#CONTENT_URI}. + * </p> + * <p> + * {@link ContentResolver#update(Uri, ContentValues, String, String[])} returns a positive + * integer when successful, and returns 0 if no contact with that id was found. + * </p> + * <p> + * Example: + * <pre> + * Uri uri = DataUsageFeedback.UPDATE_URI.buildUpon() + * .appendPath(TextUtils.join(",", dataIds)) + * .appendQueryParameter(DataUsageFeedback.METHOD, DataUsageFeedback.METHOD_CALL) + * .build(); + * boolean successful = resolver.update(uri, new ContentValues(), null, null) > 0; + * </pre> + * </p> + * @hide + */ + public static final class DataUsageFeedback { + + /** + * The content:// style URI for sending usage feedback. + * Must be used with {@link ContentResolver#update(Uri, ContentValues, String, String[])}. + */ + public static final Uri FEEDBACK_URI = + Uri.withAppendedPath(Data.CONTENT_URI, "usagefeedback"); + + /** + * <p> + * Name for query parameter specifying the type of data usage. + * </p> + */ + public static final String USAGE_TYPE = "type"; + + /** + * <p> + * Type of usage for voice interaction, which includes phone call, voice chat, and + * video chat. + * </p> + */ + public static final String USAGE_TYPE_CALL = "call"; + + /** + * <p> + * Type of usage for text interaction involving longer messages, which includes email. + * </p> + */ + public static final String USAGE_TYPE_LONG_TEXT = "long_text"; + + /** + * <p> + * Type of usage for text interaction involving shorter messages, which includes SMS, + * text chat with email addresses. + * </p> + */ + public static final String USAGE_TYPE_SHORT_TEXT = "short_text"; + } + + /** * Helper methods to display QuickContact dialogs that allow users to pivot on * a specific {@link Contacts} entry. */ diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index c9b2f97..3bc1348 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -166,7 +166,6 @@ public final class MediaStore { * If the EXTRA_OUTPUT is present, then the full-sized image will be written to the Uri * value of EXTRA_OUTPUT. * @see #EXTRA_OUTPUT - * @see #EXTRA_VIDEO_QUALITY */ public final static String ACTION_IMAGE_CAPTURE = "android.media.action.IMAGE_CAPTURE"; @@ -181,6 +180,9 @@ public final class MediaStore { * written to the standard location for videos, and the Uri of that location will be * returned in the data field of the Uri. * @see #EXTRA_OUTPUT + * @see #EXTRA_VIDEO_QUALITY + * @see #EXTRA_SIZE_LIMIT + * @see #EXTRA_DURATION_LIMIT */ public final static String ACTION_VIDEO_CAPTURE = "android.media.action.VIDEO_CAPTURE"; @@ -1094,12 +1096,6 @@ public final class MediaStore { public static final String ALBUM_KEY = "album_key"; /** - * A URI to the album art, if any - * <P>Type: TEXT</P> - */ - public static final String ALBUM_ART = "album_art"; - - /** * The track number of this song on the album, if any. * This number encodes both the track number and the * disc number. For multi-disc sets, this number will diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index eb9eb03..6ab7738 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"; @@ -2906,6 +2924,30 @@ public final class Settings { public static final String WIFI_WATCHDOG_PING_TIMEOUT_MS = "wifi_watchdog_ping_timeout_ms"; /** + * Setting to turn off walled garden test on Wi-Fi. Feature is enabled by default and + * the setting needs to be set to 0 to disable it. + * @hide + */ + public static final String WIFI_WATCHDOG_WALLED_GARDEN_TEST_ENABLED = + "wifi_watchdog_walled_garden_test_enabled"; + + /** + * The URL used for walled garden check upon a new conection. WifiWatchdogService + * fetches the URL and checks to see if {@link #WIFI_WATCHDOG_WALLED_GARDEN_PATTERN} + * is not part of the title string to notify the user on the presence of a walled garden. + * @hide + */ + public static final String WIFI_WATCHDOG_WALLED_GARDEN_URL = + "wifi_watchdog_walled_garden_url"; + + /** + * The pattern string in the fetched URL used to detect a walled garden + * @hide + */ + public static final String WIFI_WATCHDOG_WALLED_GARDEN_PATTERN = + "wifi_watchdog_walled_garden_pattern"; + + /** * The maximum number of times we will retry a connection to an access * point for which we have failed in acquiring an IP address from DHCP. * A value of N means that we will make N+1 connection attempts in all. @@ -3321,12 +3363,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_SCAN_INTERVAL_MS = "wifi_scan_interval_ms"; + 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_SUPPLICANT_SCAN_INTERVAL_MS = + "wifi_supplicant_scan_interval_ms"; /** * The interval in milliseconds at which to check packet counts on the @@ -3729,6 +3779,36 @@ public final class Settings { "setup_prepaid_detection_redir_host"; /** + * The user's preferred "dream" (interactive screensaver) component. + * + * This component will be launched by the PhoneWindowManager after the user's chosen idle + * timeout (specified by {@link #DREAM_TIMEOUT}). + * @hide + */ + public static final String DREAM_COMPONENT = + "dream_component"; + + /** + * The delay before a "dream" is started (set to 0 to disable). + * @hide + */ + public static final String DREAM_TIMEOUT = + "dream_timeout"; + + /** {@hide} */ + public static final String NETSTATS_POLL_INTERVAL = "netstats_poll_interval"; + /** {@hide} */ + public static final String NETSTATS_PERSIST_THRESHOLD = "netstats_persist_threshold"; + /** {@hide} */ + public static final String NETSTATS_SUMMARY_BUCKET_DURATION = "netstats_summary_bucket_duration"; + /** {@hide} */ + public static final String NETSTATS_SUMMARY_MAX_HISTORY = "netstats_summary_max_history"; + /** {@hide} */ + public static final String NETSTATS_DETAIL_BUCKET_DURATION = "netstats_detail_bucket_duration"; + /** {@hide} */ + public static final String NETSTATS_DETAIL_MAX_HISTORY = "netstats_detail_max_history"; + + /** * @hide */ public static final String[] SETTINGS_TO_BACKUP = { diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index 3a06b61..6585e82 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> */ @@ -690,12 +695,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/provider/VoicemailContract.java b/core/java/android/provider/VoicemailContract.java new file mode 100644 index 0000000..c397af9 --- /dev/null +++ b/core/java/android/provider/VoicemailContract.java @@ -0,0 +1,139 @@ +/* + * 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.provider; + +import android.content.Intent; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.CallLog.Calls; +/** + * The contract between the voicemail provider and applications. Contains + * definitions for the supported URIs and columns. + * + * <P>Voicemails are inserted by what is called as a "voicemail source" + * application, which is responsible for syncing voicemail data between a remote + * server and the local voicemail content provider. "voicemail source" + * application should use the source specific {@link #CONTENT_URI_SOURCE} URI + * to insert and retrieve voicemails. + * + * <P>In addition to the {@link ContentObserver} notifications the voicemail + * provider also generates broadcast intents to notify change for applications + * that are not active and therefore cannot listen to ContentObserver + * notifications. Broadcast intents with following actions are generated: + * <ul> + * <li> {@link #ACTION_NEW_VOICEMAIL} is generated for each new voicemail + * inserted. + * </li> + * <li> {@link Intent#ACTION_PROVIDER_CHANGED} is generated for any change + * made into the database, including new voicemail. + * </li> + * </ul> + * @hide + */ +// TODO: unhide when the API is approved by android-api-council +public class VoicemailContract { + /** The authority used by the voicemail provider. */ + public static final String AUTHORITY = "com.android.voicemail"; + + /** URI to insert/retrieve all voicemails. */ + public static final Uri CONTENT_URI = + Uri.parse("content://" + AUTHORITY + "/voicemail"); + /** URI to insert/retrieve voicemails by a given voicemai source. */ + public static final Uri CONTENT_URI_SOURCE = + Uri.parse("content://" + AUTHORITY + "/voicemail/source/"); + + // TODO: Move ACTION_NEW_VOICEMAIL to the Intent class. + /** Broadcast intent when a new voicemail record is inserted. */ + public static final String ACTION_NEW_VOICEMAIL = "android.intent.action.NEW_VOICEMAIL"; + /** + * Extra included in {@value Intent#ACTION_PROVIDER_CHANGED} and + * {@value #ACTION_NEW_VOICEMAIL} broadcast intents to indicate the package + * that caused the change in content provider. + * <p>Receivers of the broadcast can use this field to determine if this is + * a self change. + */ + public static final String EXTRA_CHANGED_BY = "com.android.voicemail.extra.CHANGED_BY"; + + /** The mime type for a collection of voicemails. */ + public static final String DIR_TYPE = + "vnd.android.cursor.dir/voicemails"; + + public static final class Voicemails implements BaseColumns { + /** + * Phone number of the voicemail sender. + * <P>Type: TEXT</P> + */ + public static final String NUMBER = Calls.NUMBER; + /** + * The date the voicemail was sent, in milliseconds since the epoch + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = Calls.DATE; + /** + * The duration of the voicemail in seconds. + * <P>Type: INTEGER (long)</P> + */ + public static final String DURATION = Calls.DURATION; + /** + * Whether this is a new voicemail (i.e. has not been heard). + * <P>Type: INTEGER (boolean)</P> + */ + public static final String NEW = Calls.NEW; + /** + * The mail box state of the voicemail. + * <P> Possible values: {@link #STATE_INBOX}, {@link #STATE_DELETED}, + * {@link #STATE_UNDELETED}. + * <P>Type: INTEGER</P> + */ + public static final String STATE = "state"; + /** Value of {@link #STATE} when the voicemail is in inbox. */ + public static int STATE_INBOX = 0; + /** Value of {@link #STATE} when the voicemail has been marked as deleted. */ + public static int STATE_DELETED = 1; + /** Value of {@link #STATE} when the voicemail has marked as undeleted. */ + public static int STATE_UNDELETED = 2; + /** + * Package name of the source application that inserted the voicemail. + * <P>Type: TEXT</P> + */ + public static final String SOURCE_PACKAGE = "source_package"; + /** + * Application-specific data available to the source application that + * inserted the voicemail. This is typically used to store the source + * specific message id to identify this voicemail on the remote + * voicemail server. + * <P>Type: TEXT</P> + * <P> Note that this is NOT the voicemail media content data. + */ + public static final String SOURCE_DATA = "provider_data"; + /** + * Whether the media content for this voicemail is available for + * consumption. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String HAS_CONTENT = "has_content"; + /** + * MIME type of the media content for the voicemail. + * <P>Type: TEXT</P> + */ + public static final String MIME_TYPE = "mime_type"; + /** + * Path to the media content file. Internal only field. + * @hide + */ + public static final String _DATA = "_data"; + } +} diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java index 132c346..ca2212c 100644 --- a/core/java/android/server/BluetoothA2dpService.java +++ b/core/java/android/server/BluetoothA2dpService.java @@ -83,19 +83,6 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { onBluetoothDisable(); break; } - } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { - int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, - BluetoothDevice.ERROR); - switch(bondState) { - case BluetoothDevice.BOND_BONDED: - if (getPriority(device) == BluetoothA2dp.PRIORITY_UNDEFINED) { - setPriority(device, BluetoothA2dp.PRIORITY_ON); - } - break; - case BluetoothDevice.BOND_NONE: - setPriority(device, BluetoothA2dp.PRIORITY_UNDEFINED); - break; - } } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { synchronized (this) { if (mAudioDevices.containsKey(device)) { @@ -158,7 +145,6 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { mAdapter = BluetoothAdapter.getDefaultAdapter(); mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); - mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); mIntentFilter.addAction(AudioManager.VOLUME_CHANGED_ACTION); diff --git a/core/java/android/server/BluetoothAdapterProperties.java b/core/java/android/server/BluetoothAdapterProperties.java index ae8104b..9723f60 100644 --- a/core/java/android/server/BluetoothAdapterProperties.java +++ b/core/java/android/server/BluetoothAdapterProperties.java @@ -76,14 +76,13 @@ class BluetoothAdapterProperties { for (int i = 0; i < properties.length; i++) { String name = properties[i]; String newValue = null; - int len; if (name == null) { Log.e(TAG, "Error:Adapter Property at index " + i + " is null"); continue; } if (name.equals("Devices") || name.equals("UUIDs")) { StringBuilder str = new StringBuilder(); - len = Integer.valueOf(properties[++i]); + int len = Integer.valueOf(properties[++i]); for (int j = 0; j < len; j++) { str.append(properties[++i]); str.append(","); diff --git a/core/java/android/server/BluetoothBondState.java b/core/java/android/server/BluetoothBondState.java index 2304a70..a36cd24 100644 --- a/core/java/android/server/BluetoothBondState.java +++ b/core/java/android/server/BluetoothBondState.java @@ -18,6 +18,9 @@ package android.server; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothHeadset; import android.content.Context; import android.content.Intent; import android.util.Log; @@ -68,6 +71,8 @@ class BluetoothBondState { private final Context mContext; private final BluetoothService mService; private final BluetoothInputProfileHandler mBluetoothInputProfileHandler; + private BluetoothA2dp mA2dpProxy; + private BluetoothHeadset mHeadsetProxy; BluetoothBondState(Context context, BluetoothService service) { mContext = context; @@ -126,14 +131,15 @@ class BluetoothBondState { if (state == BluetoothDevice.BOND_BONDED) { mService.addProfileState(address); + } else if (state == BluetoothDevice.BOND_BONDING) { + if (mA2dpProxy == null || mHeadsetProxy == null) { + getProfileProxy(); + } } else if (state == BluetoothDevice.BOND_NONE) { mService.removeProfileState(address); } - // HID is handled by BluetoothService, other profiles - // will be handled by their respective services. - mBluetoothInputProfileHandler.setInitialInputDevicePriority( - mService.getRemoteDevice(address), state); + setProfilePriorities(address, state); if (DBG) { Log.d(TAG, address + " bond state " + oldState + " -> " + state @@ -261,6 +267,52 @@ class BluetoothBondState { mPinAttempt.put(address, new Integer(newAttempt)); } + private void getProfileProxy() { + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (mA2dpProxy == null) { + bluetoothAdapter.getProfileProxy(mContext, mProfileServiceListener, + BluetoothProfile.A2DP); + } + + if (mHeadsetProxy == null) { + bluetoothAdapter.getProfileProxy(mContext, mProfileServiceListener, + BluetoothProfile.HEADSET); + } + } + + private void closeProfileProxy() { + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (mA2dpProxy != null) { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.A2DP, mA2dpProxy); + } + + if (mHeadsetProxy != null) { + bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mHeadsetProxy); + } + } + + private BluetoothProfile.ServiceListener mProfileServiceListener = + new BluetoothProfile.ServiceListener() { + + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (profile == BluetoothProfile.A2DP) { + mA2dpProxy = (BluetoothA2dp) proxy; + } else if (profile == BluetoothProfile.HEADSET) { + mHeadsetProxy = (BluetoothHeadset) proxy; + } + } + + public void onServiceDisconnected(int profile) { + if (profile == BluetoothProfile.A2DP) { + mA2dpProxy = null; + } else if (profile == BluetoothProfile.HEADSET) { + mHeadsetProxy = null; + } + } + }; + private void copyAutoPairingData() { FileInputStream in = null; FileOutputStream out = null; @@ -365,4 +417,30 @@ class BluetoothBondState { } } } + + // Set service priority of Hid, A2DP and Headset profiles depending on + // the bond state change + private void setProfilePriorities(String address, int state) { + BluetoothDevice remoteDevice = mService.getRemoteDevice(address); + // HID is handled by BluetoothService + mBluetoothInputProfileHandler.setInitialInputDevicePriority(remoteDevice, state); + + // Set service priority of A2DP and Headset + // We used to do the priority change in the 2 services after the broadcast + // intent reach them. But that left a small time gap that could reject + // incoming connection due to undefined priorities. + if (state == BluetoothDevice.BOND_BONDED) { + if (mA2dpProxy.getPriority(remoteDevice) == BluetoothProfile.PRIORITY_UNDEFINED) { + mA2dpProxy.setPriority(remoteDevice, BluetoothProfile.PRIORITY_ON); + } + + if (mHeadsetProxy.getPriority(remoteDevice) == BluetoothProfile.PRIORITY_UNDEFINED) { + mHeadsetProxy.setPriority(remoteDevice, BluetoothProfile.PRIORITY_ON); + } + } else if (state == BluetoothDevice.BOND_NONE) { + mA2dpProxy.setPriority(remoteDevice, BluetoothProfile.PRIORITY_UNDEFINED); + mHeadsetProxy.setPriority(remoteDevice, BluetoothProfile.PRIORITY_UNDEFINED); + } + } + } diff --git a/core/java/android/server/BluetoothInputProfileHandler.java b/core/java/android/server/BluetoothInputProfileHandler.java index e6513fd..247e297 100644 --- a/core/java/android/server/BluetoothInputProfileHandler.java +++ b/core/java/android/server/BluetoothInputProfileHandler.java @@ -60,7 +60,7 @@ final class BluetoothInputProfileHandler { return sInstance; } - synchronized boolean connectInputDevice(BluetoothDevice device, + boolean connectInputDevice(BluetoothDevice device, BluetoothDeviceProfileState state) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); if (objectPath == null || @@ -78,7 +78,7 @@ final class BluetoothInputProfileHandler { return false; } - synchronized boolean connectInputDeviceInternal(BluetoothDevice device) { + boolean connectInputDeviceInternal(BluetoothDevice device) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_CONNECTING); if (!mBluetoothService.connectInputDeviceNative(objectPath)) { @@ -88,7 +88,7 @@ final class BluetoothInputProfileHandler { return true; } - synchronized boolean disconnectInputDevice(BluetoothDevice device, + boolean disconnectInputDevice(BluetoothDevice device, BluetoothDeviceProfileState state) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); if (objectPath == null || @@ -105,7 +105,7 @@ final class BluetoothInputProfileHandler { return false; } - synchronized boolean disconnectInputDeviceInternal(BluetoothDevice device) { + boolean disconnectInputDeviceInternal(BluetoothDevice device) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); handleInputDeviceStateChange(device, BluetoothInputDevice.STATE_DISCONNECTING); if (!mBluetoothService.disconnectInputDeviceNative(objectPath)) { @@ -115,31 +115,31 @@ final class BluetoothInputProfileHandler { return true; } - synchronized int getInputDeviceConnectionState(BluetoothDevice device) { + int getInputDeviceConnectionState(BluetoothDevice device) { if (mInputDevices.get(device) == null) { return BluetoothInputDevice.STATE_DISCONNECTED; } return mInputDevices.get(device); } - synchronized List<BluetoothDevice> getConnectedInputDevices() { + List<BluetoothDevice> getConnectedInputDevices() { List<BluetoothDevice> devices = lookupInputDevicesMatchingStates( new int[] {BluetoothInputDevice.STATE_CONNECTED}); return devices; } - synchronized List<BluetoothDevice> getInputDevicesMatchingConnectionStates(int[] states) { + List<BluetoothDevice> getInputDevicesMatchingConnectionStates(int[] states) { List<BluetoothDevice> devices = lookupInputDevicesMatchingStates(states); return devices; } - synchronized int getInputDevicePriority(BluetoothDevice device) { + int getInputDevicePriority(BluetoothDevice device) { return Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.getBluetoothInputDevicePriorityKey(device.getAddress()), BluetoothInputDevice.PRIORITY_UNDEFINED); } - synchronized boolean setInputDevicePriority(BluetoothDevice device, int priority) { + boolean setInputDevicePriority(BluetoothDevice device, int priority) { if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { return false; } @@ -148,7 +148,7 @@ final class BluetoothInputProfileHandler { priority); } - synchronized List<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) { + List<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) { List<BluetoothDevice> inputDevices = new ArrayList<BluetoothDevice>(); for (BluetoothDevice device: mInputDevices.keySet()) { @@ -163,7 +163,7 @@ final class BluetoothInputProfileHandler { return inputDevices; } - private synchronized void handleInputDeviceStateChange(BluetoothDevice device, int state) { + private void handleInputDeviceStateChange(BluetoothDevice device, int state) { int prevState; if (mInputDevices.get(device) == null) { prevState = BluetoothInputDevice.STATE_DISCONNECTED; @@ -194,7 +194,7 @@ final class BluetoothInputProfileHandler { mBluetoothService.sendConnectionStateChange(device, state, prevState); } - synchronized void handleInputDevicePropertyChange(String address, boolean connected) { + void handleInputDevicePropertyChange(String address, boolean connected) { int state = connected ? BluetoothInputDevice.STATE_CONNECTED : BluetoothInputDevice.STATE_DISCONNECTED; BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); @@ -202,7 +202,7 @@ final class BluetoothInputProfileHandler { handleInputDeviceStateChange(device, state); } - synchronized void setInitialInputDevicePriority(BluetoothDevice device, int state) { + void setInitialInputDevicePriority(BluetoothDevice device, int state) { switch (state) { case BluetoothDevice.BOND_BONDED: if (getInputDevicePriority(device) == BluetoothInputDevice.PRIORITY_UNDEFINED) { diff --git a/core/java/android/server/BluetoothPanProfileHandler.java b/core/java/android/server/BluetoothPanProfileHandler.java index 8925856..0d63e19 100644 --- a/core/java/android/server/BluetoothPanProfileHandler.java +++ b/core/java/android/server/BluetoothPanProfileHandler.java @@ -76,17 +76,17 @@ final class BluetoothPanProfileHandler { } } - static synchronized BluetoothPanProfileHandler getInstance(Context context, + static BluetoothPanProfileHandler getInstance(Context context, BluetoothService service) { if (sInstance == null) sInstance = new BluetoothPanProfileHandler(context, service); return sInstance; } - synchronized boolean isTetheringOn() { + boolean isTetheringOn() { return mTetheringOn; } - synchronized boolean allowIncomingTethering() { + boolean allowIncomingTethering() { if (isTetheringOn() && getConnectedPanDevices().size() < mMaxPanDevices) return true; return false; @@ -94,7 +94,7 @@ final class BluetoothPanProfileHandler { private BroadcastReceiver mTetheringReceiver = null; - synchronized void setBluetoothTethering(boolean value) { + void setBluetoothTethering(boolean value) { if (!value) { disconnectPanServerDevices(); } @@ -104,7 +104,7 @@ final class BluetoothPanProfileHandler { filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); mTetheringReceiver = new BroadcastReceiver() { @Override - public synchronized void onReceive(Context context, Intent intent) { + public void onReceive(Context context, Intent intent) { if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF) == BluetoothAdapter.STATE_ON) { mTetheringOn = true; @@ -118,7 +118,7 @@ final class BluetoothPanProfileHandler { } } - synchronized int getPanDeviceConnectionState(BluetoothDevice device) { + int getPanDeviceConnectionState(BluetoothDevice device) { BluetoothPanDevice panDevice = mPanDevices.get(device); if (panDevice == null) { return BluetoothPan.STATE_DISCONNECTED; @@ -126,7 +126,7 @@ final class BluetoothPanProfileHandler { return panDevice.mState; } - synchronized boolean connectPanDevice(BluetoothDevice device) { + boolean connectPanDevice(BluetoothDevice device) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); if (DBG) Log.d(TAG, "connect PAN(" + objectPath + ")"); if (getPanDeviceConnectionState(device) != BluetoothPan.STATE_DISCONNECTED) { @@ -158,7 +158,7 @@ final class BluetoothPanProfileHandler { } } - private synchronized boolean disconnectPanServerDevices() { + private boolean disconnectPanServerDevices() { debugLog("disconnect all PAN devices"); for (BluetoothDevice device: mPanDevices.keySet()) { @@ -187,7 +187,7 @@ final class BluetoothPanProfileHandler { return true; } - synchronized List<BluetoothDevice> getConnectedPanDevices() { + List<BluetoothDevice> getConnectedPanDevices() { List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>(); for (BluetoothDevice device: mPanDevices.keySet()) { @@ -198,7 +198,7 @@ final class BluetoothPanProfileHandler { return devices; } - synchronized List<BluetoothDevice> getPanDevicesMatchingConnectionStates(int[] states) { + List<BluetoothDevice> getPanDevicesMatchingConnectionStates(int[] states) { List<BluetoothDevice> devices = new ArrayList<BluetoothDevice>(); for (BluetoothDevice device: mPanDevices.keySet()) { @@ -213,7 +213,7 @@ final class BluetoothPanProfileHandler { return devices; } - synchronized boolean disconnectPanDevice(BluetoothDevice device) { + boolean disconnectPanDevice(BluetoothDevice device) { String objectPath = mBluetoothService.getObjectPathFromAddress(device.getAddress()); debugLog("disconnect PAN(" + objectPath + ")"); @@ -249,7 +249,7 @@ final class BluetoothPanProfileHandler { return true; } - synchronized void handlePanDeviceStateChange(BluetoothDevice device, + void handlePanDeviceStateChange(BluetoothDevice device, String iface, int state, int role) { int prevState; String ifaceAddr = null; @@ -304,7 +304,7 @@ final class BluetoothPanProfileHandler { mBluetoothService.sendConnectionStateChange(device, state, prevState); } - synchronized void handlePanDeviceStateChange(BluetoothDevice device, + void handlePanDeviceStateChange(BluetoothDevice device, int state, int role) { handlePanDeviceStateChange(device, null, state, role); } @@ -343,7 +343,7 @@ final class BluetoothPanProfileHandler { } // configured when we start tethering - private synchronized String enableTethering(String iface) { + private String enableTethering(String iface) { debugLog("updateTetherState:" + iface); IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index b3cbf50..60bee9a 100755 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -1928,120 +1928,163 @@ public class BluetoothService extends IBluetooth.Stub { } /**** Handlers for PAN Profile ****/ + // TODO: This needs to be converted to a state machine. - public synchronized boolean isTetheringOn() { + public boolean isTetheringOn() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothPanProfileHandler.isTetheringOn(); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.isTetheringOn(); + } } - /*package*/ synchronized boolean allowIncomingTethering() { - return mBluetoothPanProfileHandler.allowIncomingTethering(); + /*package*/boolean allowIncomingTethering() { + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.allowIncomingTethering(); + } } - public synchronized void setBluetoothTethering(boolean value) { + public void setBluetoothTethering(boolean value) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - mBluetoothPanProfileHandler.setBluetoothTethering(value); + synchronized (mBluetoothPanProfileHandler) { + mBluetoothPanProfileHandler.setBluetoothTethering(value); + } } - public synchronized int getPanDeviceConnectionState(BluetoothDevice device) { + public int getPanDeviceConnectionState(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothPanProfileHandler.getPanDeviceConnectionState(device); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.getPanDeviceConnectionState(device); + } } - public synchronized boolean connectPanDevice(BluetoothDevice device) { + public boolean connectPanDevice(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - return mBluetoothPanProfileHandler.connectPanDevice(device); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.connectPanDevice(device); + } } - public synchronized List<BluetoothDevice> getConnectedPanDevices() { + public List<BluetoothDevice> getConnectedPanDevices() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothPanProfileHandler.getConnectedPanDevices(); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.getConnectedPanDevices(); + } } - public synchronized List<BluetoothDevice> getPanDevicesMatchingConnectionStates( + public List<BluetoothDevice> getPanDevicesMatchingConnectionStates( int[] states) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothPanProfileHandler.getPanDevicesMatchingConnectionStates(states); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.getPanDevicesMatchingConnectionStates(states); + } } - public synchronized boolean disconnectPanDevice(BluetoothDevice device) { + public boolean disconnectPanDevice(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - return mBluetoothPanProfileHandler.disconnectPanDevice(device); + synchronized (mBluetoothPanProfileHandler) { + return mBluetoothPanProfileHandler.disconnectPanDevice(device); + } } - /*package*/ synchronized void handlePanDeviceStateChange(BluetoothDevice device, + /*package*/void handlePanDeviceStateChange(BluetoothDevice device, String iface, int state, int role) { - mBluetoothPanProfileHandler.handlePanDeviceStateChange(device, iface, state, role); + synchronized (mBluetoothPanProfileHandler) { + mBluetoothPanProfileHandler.handlePanDeviceStateChange(device, iface, state, role); + } } - /*package*/ synchronized void handlePanDeviceStateChange(BluetoothDevice device, + /*package*/void handlePanDeviceStateChange(BluetoothDevice device, int state, int role) { - mBluetoothPanProfileHandler.handlePanDeviceStateChange(device, null, state, role); + synchronized (mBluetoothPanProfileHandler) { + mBluetoothPanProfileHandler.handlePanDeviceStateChange(device, null, state, role); + } } /**** Handlers for Input Device Profile ****/ + // This needs to be converted to state machine - public synchronized boolean connectInputDevice(BluetoothDevice device) { + public boolean connectInputDevice(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); BluetoothDeviceProfileState state = mDeviceProfileState.get(device.getAddress()); - return mBluetoothInputProfileHandler.connectInputDevice(device, state); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.connectInputDevice(device, state); + } } - public synchronized boolean connectInputDeviceInternal(BluetoothDevice device) { - return mBluetoothInputProfileHandler.connectInputDeviceInternal(device); + public boolean connectInputDeviceInternal(BluetoothDevice device) { + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.connectInputDeviceInternal(device); + } } - public synchronized boolean disconnectInputDevice(BluetoothDevice device) { + public boolean disconnectInputDevice(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); BluetoothDeviceProfileState state = mDeviceProfileState.get(device.getAddress()); - return mBluetoothInputProfileHandler.disconnectInputDevice(device, state); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.disconnectInputDevice(device, state); + } } - public synchronized boolean disconnectInputDeviceInternal(BluetoothDevice device) { - return mBluetoothInputProfileHandler.disconnectInputDeviceInternal(device); + public boolean disconnectInputDeviceInternal(BluetoothDevice device) { + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.disconnectInputDeviceInternal(device); + } } - public synchronized int getInputDeviceConnectionState(BluetoothDevice device) { + public int getInputDeviceConnectionState(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothInputProfileHandler.getInputDeviceConnectionState(device); - + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.getInputDeviceConnectionState(device); + } } - public synchronized List<BluetoothDevice> getConnectedInputDevices() { + public List<BluetoothDevice> getConnectedInputDevices() { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothInputProfileHandler.getConnectedInputDevices(); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.getConnectedInputDevices(); + } } - public synchronized List<BluetoothDevice> getInputDevicesMatchingConnectionStates( + public List<BluetoothDevice> getInputDevicesMatchingConnectionStates( int[] states) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothInputProfileHandler.getInputDevicesMatchingConnectionStates(states); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.getInputDevicesMatchingConnectionStates(states); + } } - public synchronized int getInputDevicePriority(BluetoothDevice device) { + public int getInputDevicePriority(BluetoothDevice device) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); - return mBluetoothInputProfileHandler.getInputDevicePriority(device); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.getInputDevicePriority(device); + } } - public synchronized boolean setInputDevicePriority(BluetoothDevice device, int priority) { + public boolean setInputDevicePriority(BluetoothDevice device, int priority) { mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); - return mBluetoothInputProfileHandler.setInputDevicePriority(device, priority); + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.setInputDevicePriority(device, priority); + } } - /*package*/synchronized List<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) { - return mBluetoothInputProfileHandler.lookupInputDevicesMatchingStates(states); + /*package*/List<BluetoothDevice> lookupInputDevicesMatchingStates(int[] states) { + synchronized (mBluetoothInputProfileHandler) { + return mBluetoothInputProfileHandler.lookupInputDevicesMatchingStates(states); + } } - /*package*/ synchronized void handleInputDevicePropertyChange(String address, boolean connected) { - mBluetoothInputProfileHandler.handleInputDevicePropertyChange(address, connected); + /*package*/void handleInputDevicePropertyChange(String address, boolean connected) { + synchronized (mBluetoothInputProfileHandler) { + mBluetoothInputProfileHandler.handleInputDevicePropertyChange(address, connected); + } } public boolean connectHeadset(String address) { diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 20661d7..eae7574 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -51,7 +51,7 @@ import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.View; import android.view.ViewGroup; -import android.view.ViewRoot; +import android.view.ViewAncestor; import android.view.WindowManager; import android.view.WindowManagerImpl; import android.view.WindowManagerPolicy; @@ -650,7 +650,7 @@ public abstract class WallpaperService extends Service { mWindowToken = wrapper.mWindowToken; mSurfaceHolder.setSizeFromLayout(); mInitializing = true; - mSession = ViewRoot.getWindowSession(getMainLooper()); + mSession = ViewAncestor.getWindowSession(getMainLooper()); mWindow.setSession(mSession); diff --git a/core/java/android/speech/RecognitionListener.java b/core/java/android/speech/RecognitionListener.java index 5eb71d7..bdb3ba9 100644 --- a/core/java/android/speech/RecognitionListener.java +++ b/core/java/android/speech/RecognitionListener.java @@ -70,7 +70,8 @@ public interface RecognitionListener { * * @param results the recognition results. To retrieve the results in {@code * ArrayList<String>} format use {@link Bundle#getStringArrayList(String)} with - * {@link SpeechRecognizer#RESULTS_RECOGNITION} as a parameter + * {@link SpeechRecognizer#RESULTS_RECOGNITION} as a parameter. A float array of + * confidence values might also be given in {@link SpeechRecognizer#CONFIDENCE_SCORES}. */ void onResults(Bundle results); diff --git a/core/java/android/speech/RecognizerIntent.java b/core/java/android/speech/RecognizerIntent.java index 02c324c..fd709f2 100644 --- a/core/java/android/speech/RecognizerIntent.java +++ b/core/java/android/speech/RecognizerIntent.java @@ -46,7 +46,7 @@ public class RecognizerIntent { } /** - * Starts an activity that will prompt the user for speech and sends it through a + * Starts an activity that will prompt the user for speech and send it through a * speech recognizer. The results will be returned via activity results (in * {@link Activity#onActivityResult}, if you start the intent using * {@link Activity#startActivityForResult(Intent, int)}), or forwarded via a PendingIntent @@ -81,8 +81,8 @@ public class RecognizerIntent { public static final String ACTION_RECOGNIZE_SPEECH = "android.speech.action.RECOGNIZE_SPEECH"; /** - * Starts an activity that will prompt the user for speech, sends it through a - * speech recognizer, and invokes and either displays a web search result or triggers + * Starts an activity that will prompt the user for speech, send it through a + * speech recognizer, and either display a web search result or trigger * another type of action based on the user's speech. * * <p>If you want to avoid triggering any type of action besides web search, you can use @@ -100,11 +100,13 @@ public class RecognizerIntent { * <li>{@link #EXTRA_MAX_RESULTS} * <li>{@link #EXTRA_PARTIAL_RESULTS} * <li>{@link #EXTRA_WEB_SEARCH_ONLY} + * <li>{@link #EXTRA_ORIGIN} * </ul> * * <p> Result extras (returned in the result, not to be specified in the request): * <ul> * <li>{@link #EXTRA_RESULTS} + * <li>{@link #EXTRA_CONFIDENCE_SCORES} (optional) * </ul> * * <p>NOTE: There may not be any applications installed to handle this action, so you should @@ -181,6 +183,13 @@ public class RecognizerIntent { * {@link java.util.Locale#getDefault()}. */ public static final String EXTRA_LANGUAGE = "android.speech.extra.LANGUAGE"; + + /** + * Optional value which can be used to indicate the referer url of a page in which + * speech was requested. For example, a web browser may choose to provide this for + * uses of speech on a given page. + */ + public static final String EXTRA_ORIGIN = "android.speech.extra.ORIGIN"; /** * Optional limit on the maximum number of results to return. If omitted the recognizer @@ -232,13 +241,31 @@ public class RecognizerIntent { /** * An ArrayList<String> of the recognition results when performing - * {@link #ACTION_RECOGNIZE_SPEECH}. Returned in the results; not to be specified in the - * recognition request. Only present when {@link Activity#RESULT_OK} is returned in - * an activity result. In a PendingIntent, the lack of this extra indicates failure. + * {@link #ACTION_RECOGNIZE_SPEECH}. Generally this list should be ordered in + * descending order of speech recognizer confidence. (See {@link #EXTRA_CONFIDENCE_SCORES}). + * Returned in the results; not to be specified in the recognition request. Only present + * when {@link Activity#RESULT_OK} is returned in an activity result. In a PendingIntent, + * the lack of this extra indicates failure. */ public static final String EXTRA_RESULTS = "android.speech.extra.RESULTS"; /** + * A float array of confidence scores of the recognition results when performing + * {@link #ACTION_RECOGNIZE_SPEECH}. The array should be the same size as the ArrayList + * returned in {@link #EXTRA_RESULTS}, and should contain values ranging from 0.0 to 1.0, + * or -1 to represent an unavailable confidence score. + * <p> + * Confidence values close to 1.0 indicate high confidence (the speech recognizer is + * confident that the recognition result is correct), while values close to 0.0 indicate + * low confidence. + * <p> + * Returned in the results; not to be specified in the recognition request. This extra is + * optional and might not be provided. Only present when {@link Activity#RESULT_OK} is + * returned in an activity result. + */ + public static final String EXTRA_CONFIDENCE_SCORES = "android.speech.extra.CONFIDENCE_SCORES"; + + /** * Returns the broadcast intent to fire with * {@link Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, int, String, Bundle)} * to receive details from the package that implements voice search. diff --git a/core/java/android/speech/SpeechRecognizer.java b/core/java/android/speech/SpeechRecognizer.java index cd73ba8..8fee41d 100644 --- a/core/java/android/speech/SpeechRecognizer.java +++ b/core/java/android/speech/SpeechRecognizer.java @@ -50,12 +50,26 @@ public class SpeechRecognizer { private static final String TAG = "SpeechRecognizer"; /** - * Used to retrieve an {@code ArrayList<String>} from the {@link Bundle} passed to the + * Key used to retrieve an {@code ArrayList<String>} from the {@link Bundle} passed to the * {@link RecognitionListener#onResults(Bundle)} and * {@link RecognitionListener#onPartialResults(Bundle)} methods. These strings are the possible * recognition results, where the first element is the most likely candidate. */ public static final String RESULTS_RECOGNITION = "results_recognition"; + + /** + * Key used to retrieve a float array from the {@link Bundle} passed to the + * {@link RecognitionListener#onResults(Bundle)} and + * {@link RecognitionListener#onPartialResults(Bundle)} methods. The array should be + * the same size as the ArrayList provided in {@link #RESULTS_RECOGNITION}, and should contain + * values ranging from 0.0 to 1.0, or -1 to represent an unavailable confidence score. + * <p> + * Confidence values close to 1.0 indicate high confidence (the speech recognizer is confident + * that the recognition result is correct), while values close to 0.0 indicate low confidence. + * <p> + * This value is optional and might not be provided. + */ + public static final String CONFIDENCE_SCORES = "confidence_scores"; /** Network operation timed out. */ public static final int ERROR_NETWORK_TIMEOUT = 1; diff --git a/core/java/android/speech/srec/Recognizer.java b/core/java/android/speech/srec/Recognizer.java index a03a36a..8a2bc7d 100644 --- a/core/java/android/speech/srec/Recognizer.java +++ b/core/java/android/speech/srec/Recognizer.java @@ -22,7 +22,6 @@ package android.speech.srec; -import android.util.Config; import android.util.Log; import java.io.File; diff --git a/core/java/android/speech/tts/AbstractSynthesisCallback.java b/core/java/android/speech/tts/AbstractSynthesisCallback.java new file mode 100644 index 0000000..c7a4af0 --- /dev/null +++ b/core/java/android/speech/tts/AbstractSynthesisCallback.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +/** + * Defines additional methods the synthesis callback must implement that + * are private to the TTS service implementation. + */ +abstract class AbstractSynthesisCallback implements SynthesisCallback { + /** + * Checks whether the synthesis request completed successfully. + */ + abstract boolean isDone(); + + /** + * Aborts the speech request. + * + * Can be called from multiple threads. + */ + abstract void stop(); +} diff --git a/core/java/android/speech/tts/AudioMessageParams.java b/core/java/android/speech/tts/AudioMessageParams.java new file mode 100644 index 0000000..68d8738 --- /dev/null +++ b/core/java/android/speech/tts/AudioMessageParams.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.speech.tts; + +import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher; + +class AudioMessageParams extends MessageParams { + private final BlockingMediaPlayer mPlayer; + + AudioMessageParams(UtteranceCompletedDispatcher dispatcher, + String callingApp, BlockingMediaPlayer player) { + super(dispatcher, callingApp); + mPlayer = player; + } + + BlockingMediaPlayer getPlayer() { + return mPlayer; + } + + @Override + int getType() { + return TYPE_AUDIO; + } + +} diff --git a/core/java/android/speech/tts/AudioPlaybackHandler.java b/core/java/android/speech/tts/AudioPlaybackHandler.java new file mode 100644 index 0000000..a3686b7 --- /dev/null +++ b/core/java/android/speech/tts/AudioPlaybackHandler.java @@ -0,0 +1,521 @@ +/* + * 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; + +import java.util.Iterator; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicLong; + +class AudioPlaybackHandler { + private static final String TAG = "TTS.AudioPlaybackHandler"; + private static final boolean DBG = false; + + private static final int MIN_AUDIO_BUFFER_SIZE = 8192; + + private static final int SYNTHESIS_START = 1; + private static final int SYNTHESIS_DATA_AVAILABLE = 2; + private static final int SYNTHESIS_COMPLETE_DATA_AVAILABLE = 3; + private static final int SYNTHESIS_DONE = 4; + + private static final int PLAY_AUDIO = 5; + private static final int PLAY_SILENCE = 6; + + private static final int SHUTDOWN = -1; + + private static final int DEFAULT_PRIORITY = 1; + private static final int HIGH_PRIORITY = 0; + + private final PriorityBlockingQueue<ListEntry> mQueue = + new PriorityBlockingQueue<ListEntry>(); + private final Thread mHandlerThread; + + private volatile MessageParams mCurrentParams = null; + // Used only for book keeping and error detection. + private volatile SynthesisMessageParams mLastSynthesisRequest = null; + // Used to order incoming messages in our priority queue. + private final AtomicLong mSequenceIdCtr = new AtomicLong(0); + + + AudioPlaybackHandler() { + mHandlerThread = new Thread(new MessageLoop(), "TTS.AudioPlaybackThread"); + } + + public void start() { + mHandlerThread.start(); + } + + /** + * Stops all synthesis for a given {@code token}. If the current token + * is currently being processed, an effort will be made to stop it but + * that is not guaranteed. + */ + synchronized public void stop(MessageParams token) { + if (token == null) { + return; + } + + removeMessages(token); + + if (token.getType() == MessageParams.TYPE_SYNTHESIS) { + mQueue.add(new ListEntry(SYNTHESIS_DONE, token, HIGH_PRIORITY)); + } else { + final MessageParams current = getCurrentParams(); + + if (current != null) { + if (token.getType() == MessageParams.TYPE_AUDIO) { + ((AudioMessageParams) current).getPlayer().stop(); + } else if (token.getType() == MessageParams.TYPE_SILENCE) { + ((SilenceMessageParams) current).getConditionVariable().open(); + } + } + } + } + + synchronized public void removePlaybackItems(String callingApp) { + removeMessages(callingApp); + stop(getCurrentParams()); + } + + synchronized public void removeAllItems() { + removeAllMessages(); + stop(getCurrentParams()); + } + + /** + * Shut down the audio playback thread. + */ + synchronized public void quit() { + stop(getCurrentParams()); + mQueue.add(new ListEntry(SHUTDOWN, null, HIGH_PRIORITY)); + } + + void enqueueSynthesisStart(SynthesisMessageParams token) { + mQueue.add(new ListEntry(SYNTHESIS_START, token)); + } + + void enqueueSynthesisDataAvailable(SynthesisMessageParams token) { + mQueue.add(new ListEntry(SYNTHESIS_DATA_AVAILABLE, token)); + } + + void enqueueSynthesisCompleteDataAvailable(SynthesisMessageParams token) { + mQueue.add(new ListEntry(SYNTHESIS_COMPLETE_DATA_AVAILABLE, token)); + } + + void enqueueSynthesisDone(SynthesisMessageParams token) { + mQueue.add(new ListEntry(SYNTHESIS_DONE, token)); + } + + void enqueueAudio(AudioMessageParams token) { + mQueue.add(new ListEntry(PLAY_AUDIO, token)); + } + + void enqueueSilence(SilenceMessageParams token) { + mQueue.add(new ListEntry(PLAY_SILENCE, token)); + } + + // ----------------------------------------- + // End of public API methods. + // ----------------------------------------- + + // ----------------------------------------- + // Methods for managing the message queue. + // ----------------------------------------- + + /* + * The MessageLoop is a handler like implementation that + * processes messages from a priority queue. + */ + private final class MessageLoop implements Runnable { + @Override + public void run() { + while (true) { + ListEntry entry = null; + try { + entry = mQueue.take(); + } catch (InterruptedException ie) { + return; + } + + if (entry.mWhat == SHUTDOWN) { + if (DBG) Log.d(TAG, "MessageLoop : Shutting down"); + return; + } + + if (DBG) { + Log.d(TAG, "MessageLoop : Handling message :" + entry.mWhat + + " ,seqId : " + entry.mSequenceId); + } + + setCurrentParams(entry.mMessage); + handleMessage(entry); + setCurrentParams(null); + } + } + } + + /* + * Remove all messages from the queue that contain the supplied token. + * Note that the Iterator is thread safe, and other methods can safely + * continue adding to the queue at this point. + */ + synchronized private void removeMessages(MessageParams token) { + if (token == null) { + return; + } + + Iterator<ListEntry> it = mQueue.iterator(); + + while (it.hasNext()) { + final ListEntry current = it.next(); + if (current.mMessage == token) { + it.remove(); + } + } + } + + /* + * Atomically clear the queue of all messages. + */ + synchronized private void removeAllMessages() { + mQueue.clear(); + } + + /* + * Remove all messages that originate from a given calling app. + */ + synchronized private void removeMessages(String callingApp) { + Iterator<ListEntry> it = mQueue.iterator(); + + while (it.hasNext()) { + final ListEntry current = it.next(); + // The null check is to prevent us from removing control messages, + // such as a shutdown message. + if (current.mMessage != null && + callingApp.equals(current.mMessage.getCallingApp())) { + it.remove(); + } + } + } + + /* + * An element of our priority queue of messages. Each message has a priority, + * and a sequence id (defined by the order of enqueue calls). Among messages + * with the same priority, messages that were received earlier win out. + */ + private final class ListEntry implements Comparable<ListEntry> { + final int mWhat; + final MessageParams mMessage; + final int mPriority; + final long mSequenceId; + + private ListEntry(int what, MessageParams message) { + this(what, message, DEFAULT_PRIORITY); + } + + private ListEntry(int what, MessageParams message, int priority) { + mWhat = what; + mMessage = message; + mPriority = priority; + mSequenceId = mSequenceIdCtr.incrementAndGet(); + } + + @Override + public int compareTo(ListEntry that) { + if (that == this) { + return 0; + } + + // Note that this is always 0, 1 or -1. + int priorityDiff = mPriority - that.mPriority; + if (priorityDiff == 0) { + // The == case cannot occur. + return (mSequenceId < that.mSequenceId) ? -1 : 1; + } + + return priorityDiff; + } + } + + private void setCurrentParams(MessageParams p) { + mCurrentParams = p; + } + + private MessageParams getCurrentParams() { + return mCurrentParams; + } + + // ----------------------------------------- + // Methods for dealing with individual messages, the methods + // below do the actual work. + // ----------------------------------------- + + private void handleMessage(ListEntry entry) { + final MessageParams msg = entry.mMessage; + if (entry.mWhat == SYNTHESIS_START) { + handleSynthesisStart(msg); + } else if (entry.mWhat == SYNTHESIS_DATA_AVAILABLE) { + handleSynthesisDataAvailable(msg); + } else if (entry.mWhat == SYNTHESIS_DONE) { + handleSynthesisDone(msg); + } else if (entry.mWhat == SYNTHESIS_COMPLETE_DATA_AVAILABLE) { + handleSynthesisCompleteDataAvailable(msg); + } else if (entry.mWhat == PLAY_AUDIO) { + handleAudio(msg); + } else if (entry.mWhat == PLAY_SILENCE) { + handleSilence(msg); + } + } + + // Currently implemented as blocking the audio playback thread for the + // specified duration. If a call to stop() is made, the thread + // unblocks. + private void handleSilence(MessageParams msg) { + if (DBG) Log.d(TAG, "handleSilence()"); + SilenceMessageParams params = (SilenceMessageParams) msg; + if (params.getSilenceDurationMs() > 0) { + params.getConditionVariable().block(params.getSilenceDurationMs()); + } + params.getDispatcher().dispatchUtteranceCompleted(); + if (DBG) Log.d(TAG, "handleSilence() done."); + } + + // Plays back audio from a given URI. No TTS engine involvement here. + private void handleAudio(MessageParams msg) { + if (DBG) Log.d(TAG, "handleAudio()"); + AudioMessageParams params = (AudioMessageParams) msg; + // Note that the BlockingMediaPlayer spawns a separate thread. + // + // TODO: This can be avoided. + params.getPlayer().startAndWait(); + params.getDispatcher().dispatchUtteranceCompleted(); + if (DBG) Log.d(TAG, "handleAudio() done."); + } + + // Denotes the start of a new synthesis request. We create a new + // audio track, and prepare it for incoming data. + // + // Note that since all TTS synthesis happens on a single thread, we + // should ALWAYS see the following order : + // + // handleSynthesisStart -> handleSynthesisDataAvailable(*) -> handleSynthesisDone + // OR + // handleSynthesisCompleteDataAvailable. + private void handleSynthesisStart(MessageParams msg) { + if (DBG) Log.d(TAG, "handleSynthesisStart()"); + final SynthesisMessageParams param = (SynthesisMessageParams) msg; + + // Oops, looks like the engine forgot to call done(). We go through + // extra trouble to clean the data to prevent the AudioTrack resources + // from being leaked. + if (mLastSynthesisRequest != null) { + Log.w(TAG, "Error : Missing call to done() for request : " + + mLastSynthesisRequest); + handleSynthesisDone(mLastSynthesisRequest); + } + + mLastSynthesisRequest = param; + + // Create the audio track. + final AudioTrack audioTrack = createStreamingAudioTrack( + param.mStreamType, param.mSampleRateInHz, param.mAudioFormat, + param.mChannelCount, param.mVolume, param.mPan); + + if (DBG) Log.d(TAG, "Created audio track [" + audioTrack.hashCode() + "]"); + + param.setAudioTrack(audioTrack); + } + + // More data available to be flushed to the audio track. + private void handleSynthesisDataAvailable(MessageParams msg) { + final SynthesisMessageParams param = (SynthesisMessageParams) msg; + if (param.getAudioTrack() == null) { + Log.w(TAG, "Error : null audio track in handleDataAvailable."); + return; + } + + if (param != mLastSynthesisRequest) { + Log.e(TAG, "Call to dataAvailable without done() / start()"); + return; + } + + final AudioTrack audioTrack = param.getAudioTrack(); + final SynthesisMessageParams.ListEntry bufferCopy = param.getNextBuffer(); + + if (bufferCopy == null) { + Log.e(TAG, "No buffers available to play."); + return; + } + + int playState = audioTrack.getPlayState(); + if (playState == AudioTrack.PLAYSTATE_STOPPED) { + if (DBG) Log.d(TAG, "AudioTrack stopped, restarting : " + audioTrack.hashCode()); + audioTrack.play(); + } + int count = 0; + while (count < bufferCopy.mLength) { + // Note that we don't take bufferCopy.mOffset into account because + // it is guaranteed to be 0. + int written = audioTrack.write(bufferCopy.mBytes, count, bufferCopy.mLength); + if (written <= 0) { + break; + } + count += written; + } + } + + private void handleSynthesisDone(MessageParams msg) { + final SynthesisMessageParams params = (SynthesisMessageParams) msg; + handleSynthesisDone(params); + } + + // Flush all remaining data to the audio track, stop it and release + // all it's resources. + private void handleSynthesisDone(SynthesisMessageParams params) { + if (DBG) Log.d(TAG, "handleSynthesisDone()"); + final AudioTrack audioTrack = params.getAudioTrack(); + + try { + if (audioTrack != null) { + audioTrack.flush(); + audioTrack.stop(); + if (DBG) Log.d(TAG, "Releasing audio track [" + audioTrack.hashCode() + "]"); + audioTrack.release(); + } + } finally { + params.setAudioTrack(null); + params.getDispatcher().dispatchUtteranceCompleted(); + mLastSynthesisRequest = null; + } + } + + private void handleSynthesisCompleteDataAvailable(MessageParams msg) { + final SynthesisMessageParams params = (SynthesisMessageParams) msg; + if (DBG) Log.d(TAG, "completeAudioAvailable(" + params + ")"); + + // Channel config and bytes per frame are checked before + // this message is sent. + int channelConfig = AudioPlaybackHandler.getChannelConfig(params.mChannelCount); + int bytesPerFrame = AudioPlaybackHandler.getBytesPerFrame(params.mAudioFormat); + + SynthesisMessageParams.ListEntry entry = params.getNextBuffer(); + + if (entry == null) { + Log.w(TAG, "completeDataAvailable : No buffers available to play."); + return; + } + + final AudioTrack audioTrack = new AudioTrack(params.mStreamType, params.mSampleRateInHz, + channelConfig, params.mAudioFormat, entry.mLength, AudioTrack.MODE_STATIC); + + // So that handleDone can access this correctly. + params.mAudioTrack = audioTrack; + + try { + audioTrack.write(entry.mBytes, entry.mOffset, entry.mLength); + setupVolume(audioTrack, params.mVolume, params.mPan); + audioTrack.play(); + blockUntilDone(audioTrack, bytesPerFrame, entry.mLength); + if (DBG) Log.d(TAG, "Wrote data to audio track successfully : " + entry.mLength); + } catch (IllegalStateException ex) { + Log.e(TAG, "Playback error", ex); + } finally { + handleSynthesisDone(msg); + } + } + + + private static 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(); + audioTrack.getPlayState(); + if (DBG) Log.d(TAG, "About to sleep for : " + estimatedTimeMs + " ms," + + " Playback position : " + currentPosition); + try { + Thread.sleep(estimatedTimeMs); + } catch (InterruptedException ie) { + break; + } + } + } + + private static AudioTrack createStreamingAudioTrack(int streamType, int sampleRateInHz, + int audioFormat, int channelCount, float volume, float pan) { + int channelConfig = getChannelConfig(channelCount); + + int minBufferSizeInBytes + = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat); + int bufferSizeInBytes = Math.max(MIN_AUDIO_BUFFER_SIZE, minBufferSizeInBytes); + + AudioTrack audioTrack = new AudioTrack(streamType, sampleRateInHz, channelConfig, + audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM); + if (audioTrack.getState() != AudioTrack.STATE_INITIALIZED) { + Log.w(TAG, "Unable to create audio track."); + audioTrack.release(); + return null; + } + + setupVolume(audioTrack, volume, pan); + return audioTrack; + } + + static int getChannelConfig(int channelCount) { + if (channelCount == 1) { + return AudioFormat.CHANNEL_OUT_MONO; + } else if (channelCount == 2){ + return AudioFormat.CHANNEL_OUT_STEREO; + } + + return 0; + } + + static int getBytesPerFrame(int audioFormat) { + if (audioFormat == AudioFormat.ENCODING_PCM_8BIT) { + return 1; + } else if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) { + return 2; + } + + return -1; + } + + private static 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 static float clip(float value, float min, float max) { + return value > max ? max : (value < min ? min : value); + } + +} 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/FileSynthesisCallback.java b/core/java/android/speech/tts/FileSynthesisCallback.java new file mode 100644 index 0000000..4f4b3fb --- /dev/null +++ b/core/java/android/speech/tts/FileSynthesisCallback.java @@ -0,0 +1,250 @@ +/* + * 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 FileSynthesisCallback extends AbstractSynthesisCallback { + + 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; + + FileSynthesisCallback(File fileName) { + 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/MessageParams.java b/core/java/android/speech/tts/MessageParams.java new file mode 100644 index 0000000..4c1b6d2 --- /dev/null +++ b/core/java/android/speech/tts/MessageParams.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.speech.tts; + +import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher; + +abstract class MessageParams { + static final int TYPE_SYNTHESIS = 1; + static final int TYPE_AUDIO = 2; + static final int TYPE_SILENCE = 3; + + private final UtteranceCompletedDispatcher mDispatcher; + private final String mCallingApp; + + MessageParams(UtteranceCompletedDispatcher dispatcher, String callingApp) { + mDispatcher = dispatcher; + mCallingApp = callingApp; + } + + UtteranceCompletedDispatcher getDispatcher() { + return mDispatcher; + } + + String getCallingApp() { + return mCallingApp; + } + + abstract int getType(); +} diff --git a/core/java/android/speech/tts/PlaybackSynthesisCallback.java b/core/java/android/speech/tts/PlaybackSynthesisCallback.java new file mode 100644 index 0000000..bdaa1b8 --- /dev/null +++ b/core/java/android/speech/tts/PlaybackSynthesisCallback.java @@ -0,0 +1,221 @@ +/* + * 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.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher; +import android.util.Log; + +/** + * Speech synthesis request that plays the audio as it is received. + */ +class PlaybackSynthesisCallback extends AbstractSynthesisCallback { + + 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; + + /** + * Guards {@link #mAudioTrackHandler}, {@link #mToken} and {@link #mStopped}. + */ + private final Object mStateLock = new Object(); + + // Handler associated with a thread that plays back audio requests. + private final AudioPlaybackHandler mAudioTrackHandler; + // A request "token", which will be non null after start() or + // completeAudioAvailable() have been called. + private SynthesisMessageParams mToken = null; + // Whether this request has been stopped. This is useful for keeping + // track whether stop() has been called before start(). In all other cases, + // a non-null value of mToken will provide the same information. + private boolean mStopped = false; + + private volatile boolean mDone = false; + + private final UtteranceCompletedDispatcher mDispatcher; + private final String mCallingApp; + + PlaybackSynthesisCallback(int streamType, float volume, float pan, + AudioPlaybackHandler audioTrackHandler, UtteranceCompletedDispatcher dispatcher, + String callingApp) { + mStreamType = streamType; + mVolume = volume; + mPan = pan; + mAudioTrackHandler = audioTrackHandler; + mDispatcher = dispatcher; + mCallingApp = callingApp; + } + + @Override + void stop() { + if (DBG) Log.d(TAG, "stop()"); + + synchronized (mStateLock) { + if (mToken == null || mStopped) { + Log.w(TAG, "stop() called twice, before start(), or after done()"); + return; + } + mAudioTrackHandler.stop(mToken); + mToken = null; + mStopped = true; + } + } + + @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; + } + + @Override + public int start(int sampleRateInHz, int audioFormat, int channelCount) { + if (DBG) { + Log.d(TAG, "start(" + sampleRateInHz + "," + audioFormat + + "," + channelCount + ")"); + } + + int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount); + if (channelConfig == 0) { + Log.e(TAG, "Unsupported number of channels :" + channelCount); + return TextToSpeech.ERROR; + } + + synchronized (mStateLock) { + if (mStopped) { + if (DBG) Log.d(TAG, "stop() called before start(), returning."); + return TextToSpeech.ERROR; + } + SynthesisMessageParams params = new SynthesisMessageParams( + mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan, + mDispatcher, mCallingApp); + mAudioTrackHandler.enqueueSynthesisStart(params); + + mToken = params; + } + + return TextToSpeech.SUCCESS; + } + + + @Override + public int audioAvailable(byte[] buffer, int offset, int length) { + if (DBG) { + Log.d(TAG, "audioAvailable(byte[" + buffer.length + "]," + + offset + "," + length + ")"); + } + if (length > getMaxBufferSize() || length <= 0) { + throw new IllegalArgumentException("buffer is too large or of zero length (" + + + length + " bytes)"); + } + + synchronized (mStateLock) { + if (mToken == null) { + return TextToSpeech.ERROR; + } + + // Sigh, another copy. + final byte[] bufferCopy = new byte[length]; + System.arraycopy(buffer, offset, bufferCopy, 0, length); + mToken.addBuffer(bufferCopy); + mAudioTrackHandler.enqueueSynthesisDataAvailable(mToken); + } + + return TextToSpeech.SUCCESS; + } + + @Override + public int done() { + if (DBG) Log.d(TAG, "done()"); + + synchronized (mStateLock) { + if (mDone) { + Log.w(TAG, "Duplicate call to done()"); + return TextToSpeech.ERROR; + } + + mDone = true; + + if (mToken == null) { + return TextToSpeech.ERROR; + } + + mAudioTrackHandler.enqueueSynthesisDone(mToken); + } + return TextToSpeech.SUCCESS; + } + + @Override + public void error() { + if (DBG) Log.d(TAG, "error() [will call stop]"); + stop(); + } + + @Override + public int completeAudioAvailable(int sampleRateInHz, int audioFormat, int channelCount, + byte[] buffer, int offset, int length) { + int channelConfig = AudioPlaybackHandler.getChannelConfig(channelCount); + if (channelConfig == 0) { + Log.e(TAG, "Unsupported number of channels :" + channelCount); + return TextToSpeech.ERROR; + } + + int bytesPerFrame = AudioPlaybackHandler.getBytesPerFrame(audioFormat); + if (bytesPerFrame < 0) { + Log.e(TAG, "Unsupported audio format :" + audioFormat); + return TextToSpeech.ERROR; + } + + synchronized (mStateLock) { + if (mStopped) { + return TextToSpeech.ERROR; + } + SynthesisMessageParams params = new SynthesisMessageParams( + mStreamType, sampleRateInHz, audioFormat, channelCount, mVolume, mPan, + mDispatcher, mCallingApp); + params.addBuffer(buffer, offset, length); + + mAudioTrackHandler.enqueueSynthesisCompleteDataAvailable(params); + mToken = params; + } + + return TextToSpeech.SUCCESS; + } + +} diff --git a/core/java/android/speech/tts/SilenceMessageParams.java b/core/java/android/speech/tts/SilenceMessageParams.java new file mode 100644 index 0000000..7a4ff1c --- /dev/null +++ b/core/java/android/speech/tts/SilenceMessageParams.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.os.ConditionVariable; +import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher; + +class SilenceMessageParams extends MessageParams { + private final ConditionVariable mCondVar = new ConditionVariable(); + private final long mSilenceDurationMs; + + SilenceMessageParams(UtteranceCompletedDispatcher dispatcher, + String callingApp, long silenceDurationMs) { + super(dispatcher, callingApp); + mSilenceDurationMs = silenceDurationMs; + } + + long getSilenceDurationMs() { + return mSilenceDurationMs; + } + + @Override + int getType() { + return TYPE_SILENCE; + } + + ConditionVariable getConditionVariable() { + return mCondVar; + } + +} diff --git a/core/java/android/speech/tts/SynthesisCallback.java b/core/java/android/speech/tts/SynthesisCallback.java new file mode 100644 index 0000000..1b80e40 --- /dev/null +++ b/core/java/android/speech/tts/SynthesisCallback.java @@ -0,0 +1,107 @@ +/* + * 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 callback to return speech data synthesized by a text to speech engine. + * + * 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}. + * + * {@link #error} can be called at any stage in the synthesis process to + * indicate that an error has occured, but if the call is made after a call + * to {@link #done} or {@link #completeAudioAvailable} it might be discarded. + */ +public interface SynthesisCallback { + /** + * @return 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}. + * Calls to {@link #audioAvailable} with data lengths larger than this + * value will not succeed. + */ + public int getMaxBufferSize(); + + /** + * 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 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 int audioAvailable(byte[] buffer, int offset, int length); + + /** + * 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 int completeAudioAvailable(int sampleRateInHz, int audioFormat, + int channelCount, 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 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 void error(); + +}
\ No newline at end of file diff --git a/core/java/android/speech/tts/SynthesisMessageParams.java b/core/java/android/speech/tts/SynthesisMessageParams.java new file mode 100644 index 0000000..51f3d2e --- /dev/null +++ b/core/java/android/speech/tts/SynthesisMessageParams.java @@ -0,0 +1,93 @@ +/* + * 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.AudioTrack; +import android.speech.tts.TextToSpeechService.UtteranceCompletedDispatcher; + +import java.util.LinkedList; + +/** + * Params required to play back a synthesis request. + */ +final class SynthesisMessageParams extends MessageParams { + final int mStreamType; + final int mSampleRateInHz; + final int mAudioFormat; + final int mChannelCount; + final float mVolume; + final float mPan; + + public volatile AudioTrack mAudioTrack; + + private final LinkedList<ListEntry> mDataBufferList = new LinkedList<ListEntry>(); + + SynthesisMessageParams(int streamType, int sampleRate, + int audioFormat, int channelCount, + float volume, float pan, UtteranceCompletedDispatcher dispatcher, + String callingApp) { + super(dispatcher, callingApp); + + mStreamType = streamType; + mSampleRateInHz = sampleRate; + mAudioFormat = audioFormat; + mChannelCount = channelCount; + mVolume = volume; + mPan = pan; + + // initially null. + mAudioTrack = null; + } + + @Override + int getType() { + return TYPE_SYNTHESIS; + } + + synchronized void addBuffer(byte[] buffer, int offset, int length) { + mDataBufferList.add(new ListEntry(buffer, offset, length)); + } + + synchronized void addBuffer(byte[] buffer) { + mDataBufferList.add(new ListEntry(buffer, 0, buffer.length)); + } + + synchronized ListEntry getNextBuffer() { + return mDataBufferList.poll(); + } + + + void setAudioTrack(AudioTrack audioTrack) { + mAudioTrack = audioTrack; + } + + AudioTrack getAudioTrack() { + return mAudioTrack; + } + + static final class ListEntry { + final byte[] mBytes; + final int mOffset; + final int mLength; + + ListEntry(byte[] bytes, int offset, int length) { + mBytes = bytes; + mOffset = offset; + mLength = length; + } + } +} + diff --git a/core/java/android/speech/tts/SynthesisRequest.java b/core/java/android/speech/tts/SynthesisRequest.java new file mode 100644 index 0000000..ef1704c --- /dev/null +++ b/core/java/android/speech/tts/SynthesisRequest.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.os.Bundle; + +/** + * Contains data required by engines to synthesize speech. This data is : + * <ul> + * <li>The text to synthesize</li> + * <li>The synthesis locale, represented as a language, country and a variant. + * The language is an ISO 639-3 letter language code, and the country is an + * ISO 3166 alpha 3 code. The variant is not specified.</li> + * <li>The synthesis speech rate, with 100 being the normal, and + * higher values representing higher speech rates.</li> + * <li>The voice pitch, with 100 being the default pitch.</li> + * </ul> + * + * Any additional parameters sent to the text to speech service are passed in + * uninterpreted, see the @code{params} argument in {@link TextToSpeech#speak} + * and {@link TextToSpeech#synthesizeToFile}. + */ +public final class SynthesisRequest { + private final String mText; + private final Bundle mParams; + private String mLanguage; + private String mCountry; + private String mVariant; + private int mSpeechRate; + private int mPitch; + + SynthesisRequest(String text, Bundle params) { + mText = text; + // Makes a copy of params. + mParams = new Bundle(params); + } + + /** + * Gets the text which should be synthesized. + */ + public String getText() { + return mText; + } + + /** + * Gets the ISO 3-letter language code for the language to use. + */ + public String getLanguage() { + return mLanguage; + } + + /** + * Gets the ISO 3-letter country code for the language to use. + */ + public String getCountry() { + return mCountry; + } + + /** + * Gets the language variant to use. + */ + public String getVariant() { + return mVariant; + } + + /** + * Gets the speech rate to use. The normal rate is 100. + */ + public int getSpeechRate() { + return mSpeechRate; + } + + /** + * Gets the pitch to use. The normal pitch is 100. + */ + public int getPitch() { + return mPitch; + } + + /** + * Gets the additional params, if any. + */ + public Bundle getParams() { + return mParams; + } + + /** + * 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; + } +} diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index 186af70..23fd96f 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,26 @@ */ 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.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.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; /** * @@ -44,41 +48,50 @@ 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 * and text to be synthesized) are dropped and replaced by the new entry. + * Queues are flushed with respect to a given calling app. Entries in the queue + * from other callees are not discarded. */ public static final int QUEUE_FLUSH = 0; /** * Queue mode where the new entry is added at the end of the playback queue. */ public static final int QUEUE_ADD = 1; - + /** + * Queue mode where the entire playback queue is purged. This is different + * from {@link #QUEUE_FLUSH} in that all entries are purged, not just entries + * from a given caller. + * + * @hide + */ + static final int QUEUE_DESTROY = 2; /** * Denotes the language is available exactly as specified by the locale. */ public static final int LANG_COUNTRY_VAR_AVAILABLE = 2; - /** - * Denotes the language is available for the language and country specified + * Denotes the language is available for the language and country specified * by the locale, but not the variant. */ public static final int LANG_COUNTRY_AVAILABLE = 1; - /** - * Denotes the language is available for the language by the locale, + * Denotes the language is available for the language by the locale, * but not the country and variant. */ public static final int LANG_AVAILABLE = 0; @@ -93,7 +106,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 +114,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 +121,119 @@ 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. + * + * @hide + * @deprecated No longer in use, the default engine is determined by + * the sort order defined in {@link EngineInfoComparator}. Note that + * this doesn't "break" anything because there is no guarantee that + * the engine specified below is installed on a given build, let + * alone be the default. */ - public static final String DEFAULT_SYNTH = "com.svox.pico"; + @Deprecated + public static final String DEFAULT_ENGINE = "com.svox.pico"; - // default values for rendering /** * Default audio stream used when playing synthesized speech. */ public static final int DEFAULT_STREAM = AudioManager.STREAM_MUSIC; - // return codes for a TTS engine's check data activity /** * Indicates success when checking the installation status of the resources used by the * TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_PASS = 1; + /** * Indicates failure when checking the installation status of the resources used by the * TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_FAIL = 0; + /** * Indicates erroneous data when checking the installation status of the resources used by * the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_BAD_DATA = -1; + /** * Indicates missing resources when checking the installation status of the resources used * by the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_MISSING_DATA = -2; + /** * Indicates missing storage volume when checking the installation status of the resources * used by the TextToSpeech engine with the {@link #ACTION_CHECK_TTS_DATA} intent. */ public static final int CHECK_VOICE_DATA_MISSING_VOLUME = -3; + /** + * Intent for starting a TTS service. Services that handle this intent must + * extend {@link TextToSpeechService}. Normal applications should not use this intent + * directly, instead they should talk to the TTS service using the the methods in this + * class. + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String INTENT_ACTION_TTS_SERVICE = + "android.intent.action.TTS_SERVICE"; + // intents to ask engine to install data or check its data /** * Activity Action: Triggers the platform TextToSpeech engine to @@ -229,6 +256,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 +284,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 +321,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 +329,7 @@ public class TextToSpeech { * optional (ie, "eng" or "eng-USA" or "eng-USA-FEMALE"). */ public static final String EXTRA_UNAVAILABLE_VOICES = "unavailableVoices"; + /** * Extra information sent with the {@link #ACTION_CHECK_TTS_DATA} intent where the * caller indicates to the TextToSpeech engine which specific sets of voice data to @@ -309,137 +352,106 @@ public class TextToSpeech { // keys for the parameters passed with speak commands. Hidden keys are used internally // to maintain engine state for each TextToSpeech instance. /** - * {@hide} + * @hide */ public static final String KEY_PARAM_RATE = "rate"; + /** - * {@hide} + * @hide */ public static final String KEY_PARAM_LANGUAGE = "language"; + /** - * {@hide} + * @hide */ public static final String KEY_PARAM_COUNTRY = "country"; + /** - * {@hide} + * @hide */ public static final String KEY_PARAM_VARIANT = "variant"; + /** - * {@hide} + * @hide */ public static final String KEY_PARAM_ENGINE = "engine"; + /** - * {@hide} + * @hide */ public static final String KEY_PARAM_PITCH = "pitch"; + /** * Parameter key to specify the audio stream type to be used when speaking text - * or playing back a file. + * or playing back a file. The value should be one of the STREAM_ constants + * defined in {@link AudioManager}. + * * @see TextToSpeech#speak(String, int, HashMap) * @see TextToSpeech#playEarcon(String, int, HashMap) */ public static final String KEY_PARAM_STREAM = "streamType"; + /** * Parameter key to identify an utterance in the * {@link TextToSpeech.OnUtteranceCompletedListener} after text has been * spoken, a file has been played back or a silence duration has elapsed. + * * @see TextToSpeech#speak(String, int, HashMap) * @see TextToSpeech#playEarcon(String, int, HashMap) * @see TextToSpeech#synthesizeToFile(String, HashMap, String) */ public static final String KEY_PARAM_UTTERANCE_ID = "utteranceId"; + /** * Parameter key to specify the speech volume relative to the current stream type * volume used when speaking text. Volume is specified as a float ranging from 0 to 1 * where 0 is silence, and 1 is the maximum volume (the default behavior). + * * @see TextToSpeech#speak(String, int, HashMap) * @see TextToSpeech#playEarcon(String, int, HashMap) */ public static final String KEY_PARAM_VOLUME = "volume"; + /** * Parameter key to specify how the speech is panned from left to right when speaking text. * Pan is specified as a float ranging from -1 to +1 where -1 maps to a hard-left pan, * 0 to center (the default behavior), and +1 to hard-right. + * * @see TextToSpeech#speak(String, int, HashMap) * @see TextToSpeech#playEarcon(String, int, HashMap) */ public static final String KEY_PARAM_PAN = "pan"; - // key positions in the array of cached parameters - /** - * {@hide} - */ - protected static final int PARAM_POSITION_RATE = 0; - /** - * {@hide} - */ - protected static final int PARAM_POSITION_LANGUAGE = 2; - /** - * {@hide} - */ - protected static final int PARAM_POSITION_COUNTRY = 4; - /** - * {@hide} - */ - protected static final int PARAM_POSITION_VARIANT = 6; - /** - * {@hide} - */ - protected static final int PARAM_POSITION_STREAM = 8; - /** - * {@hide} - */ - protected static final int PARAM_POSITION_UTTERANCE_ID = 10; - - /** - * {@hide} - */ - protected static final int PARAM_POSITION_ENGINE = 12; - - /** - * {@hide} - */ - protected static final int PARAM_POSITION_PITCH = 14; - - /** - * {@hide} - */ - protected static final int PARAM_POSITION_VOLUME = 16; - - /** - * {@hide} - */ - protected static final int PARAM_POSITION_PAN = 18; - - - /** - * {@hide} - * Total number of cached speech parameters. - * This number should be equal to (max param position/2) + 1. - */ - protected static final int NB_CACHED_PARAMS = 10; } - /** - * Connection needed for the TTS. - */ - private ServiceConnection mServiceConnection; - - private ITts mITts = null; - private ITtsCallback mITtscallback = null; - private Context mContext = null; - private String mPackageName = ""; - private OnInitListener mInitListener = null; - private boolean mStarted = false; + private final Context mContext; + private Connection mServiceConnection; + private OnInitListener mInitListener; private final Object mStartLock = new Object(); + + private String mRequestedEngine; + private final Map<String, Uri> mEarcons; + private final Map<String, Uri> mUtterances; + private final Bundle mParams = new Bundle(); + private final TtsEngines mEnginesHelper; + private String mCurrentEngine = null; + /** - * Used to store the cached parameters sent along with each synthesis request to the - * TTS service. + * The constructor for the TextToSpeech class, using the default TTS engine. + * This will also initialize the associated TextToSpeech engine if it isn't already running. + * + * @param context + * The context this instance is running in. + * @param listener + * The {@link TextToSpeech.OnInitListener} that will be called when the + * TextToSpeech engine has initialized. */ - private String[] mCachedParams; + public TextToSpeech(Context context, OnInitListener listener) { + this(context, listener, null); + } /** - * The constructor for the TextToSpeech class. + * The constructor for the TextToSpeech class, using the given TTS engine. * This will also initialize the associated TextToSpeech engine if it isn't already running. * * @param context @@ -447,86 +459,97 @@ public class TextToSpeech { * @param listener * The {@link TextToSpeech.OnInitListener} that will be called when the * TextToSpeech engine has initialized. + * @param engine Package name of the TTS engine to use. */ - public TextToSpeech(Context context, OnInitListener listener) { + public TextToSpeech(Context context, OnInitListener listener, String engine) { mContext = context; - mPackageName = mContext.getPackageName(); mInitListener = listener; + mRequestedEngine = engine; - mCachedParams = new String[2*Engine.NB_CACHED_PARAMS]; // store key and value - mCachedParams[Engine.PARAM_POSITION_RATE] = Engine.KEY_PARAM_RATE; - mCachedParams[Engine.PARAM_POSITION_LANGUAGE] = Engine.KEY_PARAM_LANGUAGE; - mCachedParams[Engine.PARAM_POSITION_COUNTRY] = Engine.KEY_PARAM_COUNTRY; - mCachedParams[Engine.PARAM_POSITION_VARIANT] = Engine.KEY_PARAM_VARIANT; - mCachedParams[Engine.PARAM_POSITION_STREAM] = Engine.KEY_PARAM_STREAM; - mCachedParams[Engine.PARAM_POSITION_UTTERANCE_ID] = Engine.KEY_PARAM_UTTERANCE_ID; - mCachedParams[Engine.PARAM_POSITION_ENGINE] = Engine.KEY_PARAM_ENGINE; - mCachedParams[Engine.PARAM_POSITION_PITCH] = Engine.KEY_PARAM_PITCH; - mCachedParams[Engine.PARAM_POSITION_VOLUME] = Engine.KEY_PARAM_VOLUME; - mCachedParams[Engine.PARAM_POSITION_PAN] = Engine.KEY_PARAM_PAN; - - // Leave all defaults that are shown in Settings uninitialized/at the default - // so that the values set in Settings will take effect if the application does - // not try to change these settings itself. - mCachedParams[Engine.PARAM_POSITION_RATE + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_LANGUAGE + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_COUNTRY + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_VARIANT + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_STREAM + 1] = - String.valueOf(Engine.DEFAULT_STREAM); - mCachedParams[Engine.PARAM_POSITION_UTTERANCE_ID + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_ENGINE + 1] = ""; - mCachedParams[Engine.PARAM_POSITION_PITCH + 1] = "100"; - mCachedParams[Engine.PARAM_POSITION_VOLUME + 1] = Engine.DEFAULT_VOLUME_STRING; - mCachedParams[Engine.PARAM_POSITION_PAN + 1] = Engine.DEFAULT_PAN_STRING; + mEarcons = new HashMap<String, Uri>(); + mUtterances = new HashMap<String, Uri>(); + mEnginesHelper = new TtsEngines(mContext); 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) + && mEnginesHelper.isEngineEnabled(engine)) { + engine = mRequestedEngine; + } + + // Try requested engine + if (connectToEngine(engine)) { + return SUCCESS; + } + + // Fall back to user's default engine if different from the already tested one + if (!engine.equals(defaultEngine)) { + if (connectToEngine(defaultEngine)) { + return SUCCESS; } - }; + } - Intent intent = new Intent("android.intent.action.START_TTS_SERVICE"); - intent.addCategory("android.intent.category.TTS"); - boolean bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); - if (!bound) { - Log.e("TextToSpeech.java", "initTts() failed to bind to service"); - if (mInitListener != null) { - mInitListener.onInit(ERROR); + final String highestRanked = mEnginesHelper.getHighestRankedEngineName(); + // Fall back to the hardcoded default if different from the two above + if (!defaultEngine.equals(highestRanked) + && !engine.equals(highestRanked)) { + if (connectToEngine(highestRanked)) { + return SUCCESS; } + } + + return ERROR; + } + + private boolean connectToEngine(String engine) { + Connection connection = new Connection(); + Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE); + intent.setPackage(engine); + boolean bound = mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE); + if (!bound) { + Log.e(TAG, "Failed to bind to " + engine); + dispatchOnInit(ERROR); + return false; } else { - // initialization listener will be called inside ServiceConnection - Log.i("TextToSpeech.java", "initTts() successfully bound to service"); + mCurrentEngine = engine; + return true; } - // TODO handle plugin failures } + private void dispatchOnInit(int result) { + synchronized (mStartLock) { + if (mInitListener != null) { + mInitListener.onInit(result); + mInitListener = null; + } + } + } /** * Releases the resources used by the TextToSpeech engine. @@ -534,15 +557,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 +596,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 +619,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 +652,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 +673,211 @@ public class TextToSpeech { * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ public int addEarcon(String earcon, String filename) { - synchronized (mStartLock) { - if (!mStarted) { - return ERROR; - } - try { - mITts.addEarconFile(mPackageName, earcon, filename); - return SUCCESS; - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addEarcon", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addEarcon", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - addEarcon", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } - return ERROR; + synchronized(mStartLock) { + mEarcons.put(earcon, Uri.parse(filename)); + return SUCCESS; } } + private Uri makeResourceUri(String packageName, int resourceId) { + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .encodedAuthority(packageName) + .appendEncodedPath(String.valueOf(resourceId)) + .build(); + } /** * Speaks the string using the specified queuing strategy and speech * parameters. * - * @param text - * The string of text to be spoken. - * @param queueMode - * The queuing strategy to use. - * {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. - * @param params - * The list of parameters to be used. Can be null if no parameters are given. - * They are specified using a (key, value) pair, where the key can be - * {@link Engine#KEY_PARAM_STREAM} or - * {@link Engine#KEY_PARAM_UTTERANCE_ID}. + * @param text The string of text to be spoken. + * @param queueMode The queuing strategy to use, {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. + * @param params Parameters for the request. Can be null. + * Supported parameter names: + * {@link Engine#KEY_PARAM_STREAM}, + * {@link Engine#KEY_PARAM_UTTERANCE_ID}, + * {@link Engine#KEY_PARAM_VOLUME}, + * {@link Engine#KEY_PARAM_PAN}. + * Engine specific parameters may be passed in but the parameter keys + * must be prefixed by the name of the engine they are intended for. For example + * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the + * engine named "com.svox.pico" if it is being used. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ - public int speak(String text, int queueMode, HashMap<String,String> params) - { - synchronized (mStartLock) { - int result = ERROR; - Log.i("TextToSpeech.java - speak", "speak text of length " + text.length()); - if (!mStarted) { - Log.e("TextToSpeech.java - speak", "service isn't started"); - return result; - } - try { - if ((params != null) && (!params.isEmpty())) { - setCachedParam(params, Engine.KEY_PARAM_STREAM, Engine.PARAM_POSITION_STREAM); - setCachedParam(params, Engine.KEY_PARAM_UTTERANCE_ID, - Engine.PARAM_POSITION_UTTERANCE_ID); - setCachedParam(params, Engine.KEY_PARAM_ENGINE, Engine.PARAM_POSITION_ENGINE); - setCachedParam(params, Engine.KEY_PARAM_VOLUME, Engine.PARAM_POSITION_VOLUME); - setCachedParam(params, Engine.KEY_PARAM_PAN, Engine.PARAM_POSITION_PAN); + public int speak(final String text, final int queueMode, final HashMap<String, String> params) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + Uri utteranceUri = mUtterances.get(text); + if (utteranceUri != null) { + return service.playAudio(getPackageName(), utteranceUri, queueMode, + getParams(params)); + } else { + return service.speak(getPackageName(), text, queueMode, getParams(params)); } - result = mITts.speak(mPackageName, text, queueMode, mCachedParams); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - speak", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - speak", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - speak", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - resetCachedParams(); - return result; } - } + }, ERROR, "speak"); } - /** * Plays the earcon using the specified queueing mode and parameters. + * The earcon must already have been added with {@link #addEarcon(String, String)} or + * {@link #addEarcon(String, String, int)}. * - * @param earcon - * The earcon that should be played - * @param queueMode - * {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. - * @param params - * The list of parameters to be used. Can be null if no parameters are given. - * They are specified using a (key, value) pair, where the key can be - * {@link Engine#KEY_PARAM_STREAM} or + * @param earcon The earcon that should be played + * @param queueMode {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. + * @param params Parameters for the request. Can be null. + * Supported parameter names: + * {@link Engine#KEY_PARAM_STREAM}, * {@link Engine#KEY_PARAM_UTTERANCE_ID}. + * Engine specific parameters may be passed in but the parameter keys + * must be prefixed by the name of the engine they are intended for. For example + * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the + * engine named "com.svox.pico" if it is being used. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ - public int playEarcon(String earcon, int queueMode, - HashMap<String,String> params) { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; - } - try { - if ((params != null) && (!params.isEmpty())) { - String extra = params.get(Engine.KEY_PARAM_STREAM); - if (extra != null) { - mCachedParams[Engine.PARAM_POSITION_STREAM + 1] = extra; - } - setCachedParam(params, Engine.KEY_PARAM_STREAM, Engine.PARAM_POSITION_STREAM); - setCachedParam(params, Engine.KEY_PARAM_UTTERANCE_ID, - Engine.PARAM_POSITION_UTTERANCE_ID); + public int playEarcon(final String earcon, final int queueMode, + final HashMap<String, String> params) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + Uri earconUri = mEarcons.get(earcon); + if (earconUri == null) { + return ERROR; } - result = mITts.playEarcon(mPackageName, earcon, queueMode, null); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - playEarcon", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - playEarcon", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - playEarcon", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - resetCachedParams(); - return result; + return service.playAudio(getPackageName(), earconUri, queueMode, + getParams(params)); } - } + }, ERROR, "playEarcon"); } /** * Plays silence for the specified amount of time using the specified * queue mode. * - * @param durationInMs - * A long that indicates how long the silence should last. - * @param queueMode - * {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. - * @param params - * The list of parameters to be used. Can be null if no parameters are given. - * They are specified using a (key, value) pair, where the key can be + * @param durationInMs The duration of the silence. + * @param queueMode {@link #QUEUE_ADD} or {@link #QUEUE_FLUSH}. + * @param params Parameters for the request. Can be null. + * Supported parameter names: * {@link Engine#KEY_PARAM_UTTERANCE_ID}. + * Engine specific parameters may be passed in but the parameter keys + * must be prefixed by the name of the engine they are intended for. For example + * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the + * engine named "com.svox.pico" if it is being used. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ - public int playSilence(long durationInMs, int queueMode, HashMap<String,String> params) { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; + 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; - } - try { - result = mITts.stop(mPackageName); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - stop", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - stop", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - stop", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + return service.stop(getPackageName()); } - } + }, ERROR, "stop"); } - /** - * Sets the speech rate for the TextToSpeech engine. + * Sets the speech rate. * * This has no effect on any pre-recorded speech. * - * @param speechRate - * The speech rate for the TextToSpeech engine. 1 is the normal speed, - * lower values slow down the speech (0.5 is half the normal speech rate), - * greater values accelerate it (2 is twice the normal speech rate). + * @param speechRate Speech rate. {@code 1.0} is the normal speech rate, + * lower values slow down the speech ({@code 0.5} is half the normal speech rate), + * greater values accelerate it ({@code 2.0} is twice the normal speech rate). * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ public int setSpeechRate(float speechRate) { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; - } - try { - if (speechRate > 0) { - int rate = (int)(speechRate*100); - mCachedParams[Engine.PARAM_POSITION_RATE + 1] = String.valueOf(rate); - // the rate is not set here, instead it is cached so it will be associated - // with all upcoming utterances. - if (speechRate > 0.0f) { - result = SUCCESS; - } else { - result = ERROR; - } + if (speechRate > 0.0f) { + int intRate = (int)(speechRate * 100); + if (intRate > 0) { + synchronized (mStartLock) { + mParams.putInt(Engine.KEY_PARAM_RATE, intRate); } - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setSpeechRate", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setSpeechRate", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; + return SUCCESS; } } + return ERROR; } - /** * Sets the speech pitch for the TextToSpeech engine. * * This has no effect on any pre-recorded speech. * - * @param pitch - * The pitch for the TextToSpeech engine. 1 is the normal pitch, + * @param pitch Speech pitch. {@code 1.0} is the normal pitch, * lower values lower the tone of the synthesized voice, * greater values increase it. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ public int setPitch(float pitch) { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; - } - try { - // the pitch is not set here, instead it is cached so it will be associated - // with all upcoming utterances. - if (pitch > 0) { - int p = (int)(pitch*100); - mCachedParams[Engine.PARAM_POSITION_PITCH + 1] = String.valueOf(p); - result = SUCCESS; + if (pitch > 0.0f) { + int intPitch = (int)(pitch * 100); + if (intPitch > 0) { + synchronized (mStartLock) { + mParams.putInt(Engine.KEY_PARAM_PITCH, intPitch); } - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setPitch", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setPitch", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; + return SUCCESS; } } + return ERROR; } - /** - * Sets the language for the TextToSpeech engine. - * The TextToSpeech engine will try to use the closest match to the specified + * Sets the text-to-speech language. + * The TTS engine will try to use the closest match to the specified * language as represented by the Locale, but there is no guarantee that the exact same Locale * will be used. Use {@link #isLanguageAvailable(Locale)} to check the level of support * before choosing the language to use for the next utterances. * - * @param loc - * The locale describing the language to be used. + * @param loc The locale describing the language to be used. * - * @return code indicating the support status for the locale. See {@link #LANG_AVAILABLE}, + * @return Code indicating the support status for the locale. See {@link #LANG_AVAILABLE}, * {@link #LANG_COUNTRY_AVAILABLE}, {@link #LANG_COUNTRY_VAR_AVAILABLE}, * {@link #LANG_MISSING_DATA} and {@link #LANG_NOT_SUPPORTED}. */ - public int setLanguage(Locale loc) { - synchronized (mStartLock) { - int result = LANG_NOT_SUPPORTED; - if (!mStarted) { - return result; - } - if (loc == null) { - return result; - } - try { + public int setLanguage(final Locale loc) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + if (loc == null) { + return LANG_NOT_SUPPORTED; + } String language = loc.getISO3Language(); String country = loc.getISO3Country(); String variant = loc.getVariant(); @@ -1126,386 +885,306 @@ public class TextToSpeech { // the available parts. // Note that the language is not actually set here, instead it is cached so it // will be associated with all upcoming utterances. - result = mITts.isLanguageAvailable(language, country, variant, mCachedParams); + int result = service.loadLanguage(language, country, variant); if (result >= LANG_AVAILABLE){ - mCachedParams[Engine.PARAM_POSITION_LANGUAGE + 1] = language; - if (result >= LANG_COUNTRY_AVAILABLE){ - mCachedParams[Engine.PARAM_POSITION_COUNTRY + 1] = country; - } else { - mCachedParams[Engine.PARAM_POSITION_COUNTRY + 1] = ""; - } - if (result >= LANG_COUNTRY_VAR_AVAILABLE){ - mCachedParams[Engine.PARAM_POSITION_VARIANT + 1] = variant; - } else { - mCachedParams[Engine.PARAM_POSITION_VARIANT + 1] = ""; + if (result < LANG_COUNTRY_VAR_AVAILABLE) { + variant = ""; + if (result < LANG_COUNTRY_AVAILABLE) { + country = ""; + } } + mParams.putString(Engine.KEY_PARAM_LANGUAGE, language); + mParams.putString(Engine.KEY_PARAM_COUNTRY, country); + mParams.putString(Engine.KEY_PARAM_VARIANT, variant); } - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setLanguage", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setLanguage", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setLanguage", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { return result; } - } + }, LANG_NOT_SUPPORTED, "setLanguage"); } - /** * Returns a Locale instance describing the language currently being used by the TextToSpeech * engine. + * * @return language, country (if any) and variant (if any) used by the engine stored in a Locale - * instance, or null is the TextToSpeech engine has failed. + * instance, or {@code null} on error. */ public Locale getLanguage() { - synchronized (mStartLock) { - if (!mStarted) { - return null; - } - try { - // Only do a call to the native synth if there is nothing in the cached params - if (mCachedParams[Engine.PARAM_POSITION_LANGUAGE + 1].length() < 1){ - String[] locStrings = mITts.getLanguage(); - if ((locStrings != null) && (locStrings.length == 3)) { - return new Locale(locStrings[0], locStrings[1], locStrings[2]); - } else { - return null; - } - } else { - return new Locale(mCachedParams[Engine.PARAM_POSITION_LANGUAGE + 1], - mCachedParams[Engine.PARAM_POSITION_COUNTRY + 1], - mCachedParams[Engine.PARAM_POSITION_VARIANT + 1]); + return runAction(new Action<Locale>() { + @Override + public Locale run(ITextToSpeechService service) throws RemoteException { + String[] locStrings = service.getLanguage(); + if (locStrings != null && locStrings.length == 3) { + return new Locale(locStrings[0], locStrings[1], locStrings[2]); } - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - getLanguage", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - getLanguage", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - getLanguage", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); + return null; } - return null; - } + }, null, "getLanguage"); } /** * Checks if the specified language as represented by the Locale is available and supported. * - * @param loc - * The Locale describing the language to be used. + * @param loc The Locale describing the language to be used. * - * @return code indicating the support status for the locale. See {@link #LANG_AVAILABLE}, + * @return Code indicating the support status for the locale. See {@link #LANG_AVAILABLE}, * {@link #LANG_COUNTRY_AVAILABLE}, {@link #LANG_COUNTRY_VAR_AVAILABLE}, * {@link #LANG_MISSING_DATA} and {@link #LANG_NOT_SUPPORTED}. */ - public int isLanguageAvailable(Locale loc) { - synchronized (mStartLock) { - int result = LANG_NOT_SUPPORTED; - if (!mStarted) { - return result; + public int isLanguageAvailable(final Locale loc) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + return service.isLanguageAvailable(loc.getISO3Language(), + loc.getISO3Country(), loc.getVariant()); } - try { - result = mITts.isLanguageAvailable(loc.getISO3Language(), - loc.getISO3Country(), loc.getVariant(), mCachedParams); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - isLanguageAvailable", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - isLanguageAvailable", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - isLanguageAvailable", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; - } - } + }, LANG_NOT_SUPPORTED, "isLanguageAvailable"); } - /** * Synthesizes the given text to a file using the specified parameters. * - * @param text - * The String of text that should be synthesized - * @param params - * The list of parameters to be used. Can be null if no parameters are given. - * They are specified using a (key, value) pair, where the key can be + * @param text Thetext that should be synthesized + * @param params Parameters for the request. Can be null. + * Supported parameter names: * {@link Engine#KEY_PARAM_UTTERANCE_ID}. - * @param filename - * The string that gives the full output filename; it should be + * Engine specific parameters may be passed in but the parameter keys + * must be prefixed by the name of the engine they are intended for. For example + * the keys "com.svox.pico_foo" and "com.svox.pico:bar" will be passed to the + * engine named "com.svox.pico" if it is being used. + * @param filename Absolute file filename to write the generated audio data to.It should be * something like "/sdcard/myappsounds/mysound.wav". * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ - public int synthesizeToFile(String text, HashMap<String,String> params, - String filename) { - Log.i("TextToSpeech.java", "synthesizeToFile()"); - synchronized (mStartLock) { - int result = ERROR; - Log.i("TextToSpeech.java - synthesizeToFile", "synthesizeToFile text of length " - + text.length()); - if (!mStarted) { - Log.e("TextToSpeech.java - synthesizeToFile", "service isn't started"); - return result; + public int synthesizeToFile(final String text, final HashMap<String, String> params, + final String filename) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + return service.synthesizeToFile(getPackageName(), text, filename, + getParams(params)); } - try { - if ((params != null) && (!params.isEmpty())) { - // no need to read the stream type here - setCachedParam(params, Engine.KEY_PARAM_UTTERANCE_ID, - Engine.PARAM_POSITION_UTTERANCE_ID); - setCachedParam(params, Engine.KEY_PARAM_ENGINE, Engine.PARAM_POSITION_ENGINE); + }, ERROR, "synthesizeToFile"); + } + + private Bundle getParams(HashMap<String, String> params) { + if (params != null && !params.isEmpty()) { + Bundle bundle = new Bundle(mParams); + copyIntParam(bundle, params, Engine.KEY_PARAM_STREAM); + copyStringParam(bundle, params, Engine.KEY_PARAM_UTTERANCE_ID); + copyFloatParam(bundle, params, Engine.KEY_PARAM_VOLUME); + copyFloatParam(bundle, params, Engine.KEY_PARAM_PAN); + + // Copy over all parameters that start with the name of the + // engine that we are currently connected to. The engine is + // free to interpret them as it chooses. + if (!TextUtils.isEmpty(mCurrentEngine)) { + for (Map.Entry<String, String> entry : params.entrySet()) { + final String key = entry.getKey(); + if (key != null && key.startsWith(mCurrentEngine)) { + bundle.putString(key, entry.getValue()); + } } - result = mITts.synthesizeToFile(mPackageName, text, mCachedParams, filename) ? - SUCCESS : ERROR; - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - synthesizeToFile", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - synthesizeToFile", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - synthesizeToFile", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - resetCachedParams(); - return result; } + + return bundle; + } else { + return mParams; } } + private void copyStringParam(Bundle bundle, HashMap<String, String> params, String key) { + String value = params.get(key); + if (value != null) { + bundle.putString(key, value); + } + } - /** - * Convenience method to reset the cached parameters to the current default values - * if they are not persistent between calls to the service. - */ - private void resetCachedParams() { - mCachedParams[Engine.PARAM_POSITION_STREAM + 1] = - String.valueOf(Engine.DEFAULT_STREAM); - mCachedParams[Engine.PARAM_POSITION_UTTERANCE_ID+ 1] = ""; - mCachedParams[Engine.PARAM_POSITION_VOLUME + 1] = Engine.DEFAULT_VOLUME_STRING; - mCachedParams[Engine.PARAM_POSITION_PAN + 1] = Engine.DEFAULT_PAN_STRING; + private void copyIntParam(Bundle bundle, HashMap<String, String> params, String key) { + String valueString = params.get(key); + if (!TextUtils.isEmpty(valueString)) { + try { + int value = Integer.parseInt(valueString); + bundle.putInt(key, value); + } catch (NumberFormatException ex) { + // don't set the value in the bundle + } + } } - /** - * Convenience method to save a parameter in the cached parameter array, at the given index, - * for a property saved in the given hashmap. - */ - private void setCachedParam(HashMap<String,String> params, String key, int keyIndex) { - String extra = params.get(key); - if (extra != null) { - mCachedParams[keyIndex+1] = extra; + private void copyFloatParam(Bundle bundle, HashMap<String, String> params, String key) { + String valueString = params.get(key); + if (!TextUtils.isEmpty(valueString)) { + try { + float value = Float.parseFloat(valueString); + bundle.putFloat(key, value); + } catch (NumberFormatException ex) { + // don't set the value in the bundle + } } } /** - * Sets the OnUtteranceCompletedListener that will fire when an utterance completes. + * Sets the listener that will be notified when synthesis of an utterance completes. * - * @param listener - * The OnUtteranceCompletedListener + * @param listener The listener to use. * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ - public int setOnUtteranceCompletedListener( - final OnUtteranceCompletedListener listener) { - synchronized (mStartLock) { - int result = ERROR; - if (!mStarted) { - return result; - } - mITtscallback = new ITtsCallback.Stub() { - public void utteranceCompleted(String utteranceId) throws RemoteException { - if (listener != null) { - listener.onUtteranceCompleted(utteranceId); + public int setOnUtteranceCompletedListener(final OnUtteranceCompletedListener listener) { + return runAction(new Action<Integer>() { + @Override + public Integer run(ITextToSpeechService service) throws RemoteException { + ITextToSpeechCallback.Stub callback = new ITextToSpeechCallback.Stub() { + public void utteranceCompleted(String utteranceId) { + if (listener != null) { + listener.onUtteranceCompleted(utteranceId); + } } - } - }; - try { - result = mITts.registerCallback(mPackageName, mITtscallback); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - registerCallback", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - registerCallback", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - registerCallback", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; + }; + service.setCallback(getPackageName(), callback); + return SUCCESS; } - } + }, ERROR, "setOnUtteranceCompletedListener"); } /** - * Sets the speech synthesis engine to be used by its packagename. + * Sets the TTS engine to use. * - * @param enginePackageName - * The packagename for the synthesis engine (ie, "com.svox.pico") + * @param enginePackageName The package name for the synthesis engine (e.g. "com.svox.pico") * - * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + * @return {@link #ERROR} or {@link #SUCCESS}. */ + // TODO: add @Deprecated{This method does not tell the caller when the new engine + // has been initialized. You should create a new TextToSpeech object with the new + // engine instead.} public int setEngineByPackageName(String enginePackageName) { - synchronized (mStartLock) { - int result = TextToSpeech.ERROR; - if (!mStarted) { - return result; - } - try { - result = mITts.setEngineByPackageName(enginePackageName); - if (result == TextToSpeech.SUCCESS){ - mCachedParams[Engine.PARAM_POSITION_ENGINE + 1] = enginePackageName; - } - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return result; - } - } + mRequestedEngine = enginePackageName; + return initTts(); } - /** - * Gets the packagename of the default speech synthesis engine. + * Gets the package name of the default speech synthesis engine. * - * @return Packagename of the TTS engine that the user has chosen as their default. + * @return Package name of the TTS engine that the user has chosen + * as their default. */ public String getDefaultEngine() { - synchronized (mStartLock) { - String engineName = ""; - if (!mStarted) { - return engineName; + return mEnginesHelper.getDefaultEngine(); + } + + /** + * 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; + } + + /** + * Gets a list of all installed TTS engines. + * + * @return A list of engine info objects. The list can be empty, but never {@code null}. + */ + public List<EngineInfo> getEngines() { + return mEnginesHelper.getEngines(); + } + + + private class Connection implements ServiceConnection { + private ITextToSpeechService mService; + + public void onServiceConnected(ComponentName name, IBinder service) { + Log.i(TAG, "Connected to " + name); + synchronized(mStartLock) { + if (mServiceConnection != null) { + // Disconnect any previous service connection + mServiceConnection.disconnect(); + } + mServiceConnection = this; + mService = ITextToSpeechService.Stub.asInterface(service); + dispatchOnInit(SUCCESS); } + } + + public void onServiceDisconnected(ComponentName name) { + synchronized(mStartLock) { + mService = null; + // If this is the active connection, clear it + if (mServiceConnection == this) { + mServiceConnection = null; + } + } + } + + public void disconnect() { + mContext.unbindService(this); + } + + public <R> R runAction(Action<R> action, R errorResult, String method, boolean reconnect) { try { - engineName = mITts.getDefaultEngine(); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - setEngineByPackageName", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return engineName; + synchronized (mStartLock) { + if (mService == null) { + Log.w(TAG, method + " failed: not connected to TTS engine"); + return errorResult; + } + return action.run(mService); + } + } catch (RemoteException ex) { + Log.e(TAG, method + " failed", ex); + if (reconnect) { + disconnect(); + initTts(); + } + return errorResult; } } } + private interface Action<R> { + R run(ITextToSpeechService service) throws RemoteException; + } /** - * Returns whether or not the user is forcing their defaults to override the - * Text-To-Speech settings set by applications. + * Information about an installed text-to-speech engine. * - * @return Whether or not defaults are enforced. + * @see TextToSpeech#getEngines */ - public boolean areDefaultsEnforced() { - synchronized (mStartLock) { - boolean defaultsEnforced = false; - if (!mStarted) { - return defaultsEnforced; - } - try { - defaultsEnforced = mITts.areDefaultsEnforced(); - } catch (RemoteException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - areDefaultsEnforced", "RemoteException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (NullPointerException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - areDefaultsEnforced", "NullPointerException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } catch (IllegalStateException e) { - // TTS died; restart it. - Log.e("TextToSpeech.java - areDefaultsEnforced", "IllegalStateException"); - e.printStackTrace(); - mStarted = false; - initTts(); - } finally { - return defaultsEnforced; - } + public static class EngineInfo { + /** + * Engine package name.. + */ + public String name; + /** + * Localized label for the engine. + */ + public String label; + /** + * Icon for the engine. + */ + public int icon; + /** + * Whether this engine is a part of the system + * image. + * + * @hide + */ + public boolean system; + /** + * The priority the engine declares for the the intent filter + * {@code android.intent.action.TTS_SERVICE} + * + * @hide + */ + public int priority; + + @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..3eea6b7 --- /dev/null +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -0,0 +1,859 @@ +/* + * 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.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. The following methods + * need to be implemented. + * + * <ul> + * <li>{@link #onIsLanguageAvailable}</li> + * <li>{@link #onLoadLanguage}</li> + * <li>{@link #onGetLanguage}</li> + * <li>{@link #onSynthesizeText}</li> + * <li>{@link #onStop}</li> + * </ul> + * + * The first three deal primarily with language management, and are used to + * query the engine for it's support for a given language and indicate to it + * that requests in a given language are imminent. + * + * {@link #onSynthesizeText} is central to the engine implementation. The + * implementation should synthesize text as per the request parameters and + * return synthesized data via the supplied callback. This class and its helpers + * will then consume that data, which might mean queueing it for playback or writing + * it to a file or similar. All calls to this method will be on a single + * thread, which will be different from the main thread of the service. Synthesis + * must be synchronous which means the engine must NOT hold on the callback or call + * any methods on it after the method returns + * + * {@link #onStop} tells the engine that it should stop all ongoing synthesis, if + * any. Any pending data from the current synthesis will be discarded. + */ +// TODO: Add a link to the sample TTS engine once it's done. +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; + // A thread and it's associated handler for playing back any audio + // associated with this TTS engine. Will handle all requests except synthesis + // to file requests, which occur on the synthesis thread. + private AudioPlaybackHandler mAudioPlaybackHandler; + + private CallbackMap mCallbacks; + + private int mDefaultAvailability = TextToSpeech.LANG_NOT_SUPPORTED; + + @Override + public void onCreate() { + if (DBG) Log.d(TAG, "onCreate()"); + super.onCreate(); + + SynthThread synthThread = new SynthThread(); + synthThread.start(); + mSynthHandler = new SynthHandler(synthThread.getLooper()); + + mAudioPlaybackHandler = new AudioPlaybackHandler(); + mAudioPlaybackHandler.start(); + + mCallbacks = new CallbackMap(); + + // Load default language + mDefaultAvailability = onLoadLanguage(getDefaultLanguage(), + getDefaultCountry(), getDefaultVariant()); + } + + @Override + public void onDestroy() { + if (DBG) Log.d(TAG, "onDestroy()"); + + // Tell the synthesizer to stop + mSynthHandler.quit(); + // Tell the audio playback thread to stop. + mAudioPlaybackHandler.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. + * @param callback The callback the the engine must use to make data available for + * playback or for writing to a file. + */ + protected abstract void onSynthesizeText(SynthesisRequest request, + SynthesisCallback callback); + + 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 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; + } + + if (queueMode == TextToSpeech.QUEUE_FLUSH) { + stop(speechItem.getCallingApp()); + } else if (queueMode == TextToSpeech.QUEUE_DESTROY) { + // Stop the current speech item. + stop(speechItem.getCallingApp()); + // Remove all other items from the queue. + removeCallbacksAndMessages(null); + // Remove all pending playback as well. + mAudioPlaybackHandler.removeAllItems(); + } + Runnable runnable = new Runnable() { + @Override + public void run() { + setCurrentSpeechItem(speechItem); + speechItem.play(); + setCurrentSpeechItem(null); + } + }; + Message msg = Message.obtain(this, runnable); + // The obj is used to remove all callbacks from the given app in stop(String). + // + // Note that this string is interned, so the == comparison works. + 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(); + } + + // Remove any enqueued audio too. + mAudioPlaybackHandler.removePlaybackItems(callingApp); + + return TextToSpeech.SUCCESS; + } + } + + interface UtteranceCompletedDispatcher { + public void dispatchUtteranceCompleted(); + } + + /** + * An item in the synth thread queue. + */ + private abstract class SpeechItem implements UtteranceCompletedDispatcher { + private final String mCallingApp; + protected final Bundle mParams; + private boolean mStarted = false; + private boolean mStopped = false; + + public SpeechItem(String callingApp, Bundle params) { + mCallingApp = callingApp; + mParams = params; + } + + public String getCallingApp() { + return mCallingApp; + } + + /** + * Checker whether the item is valid. If this method returns false, the item should not + * be played. + */ + public abstract boolean isValid(); + + /** + * Plays the speech item. Blocks until playback is finished. + * Must not be called more than once. + * + * Only called on the synthesis thread. + * + * @return {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. + */ + public int play() { + synchronized (this) { + if (mStarted) { + throw new IllegalStateException("play() called twice"); + } + mStarted = true; + } + return playImpl(); + } + + /** + * Stops the speech item. + * Must not be called more than once. + * + * Can be called on multiple threads, but not on the synthesis thread. + */ + public void stop() { + synchronized (this) { + if (mStopped) { + throw new IllegalStateException("stop() called twice"); + } + mStopped = true; + } + stopImpl(); + } + + public void dispatchUtteranceCompleted() { + final String utteranceId = getUtteranceId(); + if (!TextUtils.isEmpty(utteranceId)) { + mCallbacks.dispatchUtteranceCompleted(getCallingApp(), utteranceId); + } + } + + 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); + } + } + + class SynthesisSpeechItem extends SpeechItem { + private final String mText; + private final SynthesisRequest mSynthesisRequest; + // Non null after synthesis has started, and all accesses + // guarded by 'this'. + private AbstractSynthesisCallback mSynthesisCallback; + + public SynthesisSpeechItem(String callingApp, Bundle params, String text) { + super(callingApp, params); + mText = text; + mSynthesisRequest = new SynthesisRequest(mText, mParams); + setRequestParams(mSynthesisRequest); + } + + 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() { + AbstractSynthesisCallback synthesisCallback; + synchronized (this) { + mSynthesisCallback = createSynthesisCallback(); + synthesisCallback = mSynthesisCallback; + } + TextToSpeechService.this.onSynthesizeText(mSynthesisRequest, synthesisCallback); + return synthesisCallback.isDone() ? TextToSpeech.SUCCESS : TextToSpeech.ERROR; + } + + protected AbstractSynthesisCallback createSynthesisCallback() { + return new PlaybackSynthesisCallback(getStreamType(), getVolume(), getPan(), + mAudioPlaybackHandler, this, getCallingApp()); + } + + 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() { + AbstractSynthesisCallback synthesisCallback; + synchronized (this) { + synthesisCallback = mSynthesisCallback; + } + synthesisCallback.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 AbstractSynthesisCallback createSynthesisCallback() { + return new FileSynthesisCallback(mFile); + } + + @Override + protected int playImpl() { + int status = super.playImpl(); + if (status == TextToSpeech.SUCCESS) { + dispatchUtteranceCompleted(); + } + return status; + } + + /** + * 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; + private AudioMessageParams mToken; + + 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() { + mToken = new AudioMessageParams(this, getCallingApp(), mPlayer); + mAudioPlaybackHandler.enqueueAudio(mToken); + return TextToSpeech.SUCCESS; + } + + @Override + protected void stopImpl() { + if (mToken != null) { + mAudioPlaybackHandler.stop(mToken); + } + } + } + + private class SilenceSpeechItem extends SpeechItem { + private final long mDuration; + private SilenceMessageParams mToken; + + public SilenceSpeechItem(String callingApp, Bundle params, long duration) { + super(callingApp, params); + mDuration = duration; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + protected int playImpl() { + mToken = new SilenceMessageParams(this, getCallingApp(), mDuration); + mAudioPlaybackHandler.enqueueSilence(mToken); + return TextToSpeech.SUCCESS; + } + + @Override + protected void stopImpl() { + if (mToken != null) { + mAudioPlaybackHandler.stop(mToken); + } + } + } + + @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. + */ + // NOTE: All calls that are passed in a calling app are interned so that + // they can be used as message objects (which are tested for equality using ==). + private final ITextToSpeechService.Stub mBinder = new ITextToSpeechService.Stub() { + + public int speak(String callingApp, String text, int queueMode, Bundle params) { + if (!checkNonNull(callingApp, text, params)) { + return TextToSpeech.ERROR; + } + + SpeechItem item = new SynthesisSpeechItem(intern(callingApp), params, text); + return mSynthHandler.enqueueSpeechItem(queueMode, item); + } + + public int synthesizeToFile(String callingApp, String text, String filename, + Bundle params) { + if (!checkNonNull(callingApp, text, filename, params)) { + return TextToSpeech.ERROR; + } + + File file = new File(filename); + SpeechItem item = new SynthesisToFileSpeechItem(intern(callingApp), + params, text, file); + return mSynthHandler.enqueueSpeechItem(TextToSpeech.QUEUE_ADD, item); + } + + public int playAudio(String callingApp, Uri audioUri, int queueMode, Bundle params) { + if (!checkNonNull(callingApp, audioUri, params)) { + return TextToSpeech.ERROR; + } + + SpeechItem item = new AudioSpeechItem(intern(callingApp), params, audioUri); + return mSynthHandler.enqueueSpeechItem(queueMode, item); + } + + public int playSilence(String callingApp, long duration, int queueMode, Bundle params) { + if (!checkNonNull(callingApp, params)) { + return TextToSpeech.ERROR; + } + + SpeechItem item = new SilenceSpeechItem(intern(callingApp), params, duration); + return mSynthHandler.enqueueSpeechItem(queueMode, item); + } + + public boolean isSpeaking() { + return mSynthHandler.isSpeaking(); + } + + public int stop(String callingApp) { + if (!checkNonNull(callingApp)) { + return TextToSpeech.ERROR; + } + + return mSynthHandler.stop(intern(callingApp)); + } + + public String[] getLanguage() { + return onGetLanguage(); + } + + /* + * If defaults are enforced, then no language is "available" except + * perhaps the default language selected by the user. + */ + public int isLanguageAvailable(String lang, String country, String variant) { + if (!checkNonNull(lang)) { + return TextToSpeech.ERROR; + } + + if (areDefaultsEnforced()) { + if (isDefault(lang, country, variant)) { + return mDefaultAvailability; + } else { + return TextToSpeech.LANG_NOT_SUPPORTED; + } + } + return onIsLanguageAvailable(lang, country, variant); + } + + /* + * There is no point loading a non default language if defaults + * are enforced. + */ + public int loadLanguage(String lang, String country, String variant) { + if (!checkNonNull(lang)) { + return TextToSpeech.ERROR; + } + + if (areDefaultsEnforced()) { + if (isDefault(lang, country, variant)) { + return mDefaultAvailability; + } else { + return TextToSpeech.LANG_NOT_SUPPORTED; + } + } + return onLoadLanguage(lang, country, variant); + } + + public void setCallback(String packageName, ITextToSpeechCallback cb) { + // Note that passing in a null callback is a valid use case. + if (!checkNonNull(packageName)) { + return; + } + + mCallbacks.setCallback(packageName, cb); + } + + private boolean isDefault(String lang, String country, String variant) { + return Locale.getDefault().equals(new Locale(lang, country, variant)); + } + + private String intern(String in) { + // The input parameter will be non null. + return in.intern(); + } + + private boolean checkNonNull(Object... args) { + for (Object o : args) { + if (o == null) return false; + } + return true; + } + }; + + 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/speech/tts/TtsEngines.java b/core/java/android/speech/tts/TtsEngines.java new file mode 100644 index 0000000..e8d74eb --- /dev/null +++ b/core/java/android/speech/tts/TtsEngines.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package android.speech.tts; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.provider.Settings; +import android.speech.tts.TextToSpeech.Engine; +import android.speech.tts.TextToSpeech.EngineInfo; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Support class for querying the list of available engines + * on the device and deciding which one to use etc. + * + * Comments in this class the use the shorthand "system engines" for engines that + * are a part of the system image. + * + * @hide + */ +public class TtsEngines { + private final Context mContext; + + public TtsEngines(Context ctx) { + mContext = ctx; + } + + /** + * @return the default TTS engine. If the user has set a default, that + * value is returned else the highest ranked engine is returned, + * as per {@link EngineInfoComparator}. + */ + public String getDefaultEngine() { + String engine = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.TTS_DEFAULT_SYNTH); + return engine != null ? engine : getHighestRankedEngineName(); + } + + /** + * @return the package name of the highest ranked system engine, {@code null} + * if no TTS engines were present in the system image. + */ + public String getHighestRankedEngineName() { + final List<EngineInfo> engines = getEngines(); + + if (engines.size() > 0 && engines.get(0).system) { + return engines.get(0).name; + } + + return null; + } + + /** + * Returns the engine info for a given engine name. Note that engines are + * identified by their package name. + */ + public EngineInfo getEngineInfo(String packageName) { + PackageManager pm = mContext.getPackageManager(); + Intent intent = new Intent(Engine.INTENT_ACTION_TTS_SERVICE); + intent.setPackage(packageName); + List<ResolveInfo> resolveInfos = pm.queryIntentServices(intent, + PackageManager.MATCH_DEFAULT_ONLY); + // Note that the current API allows only one engine per + // package name. Since the "engine name" is the same as + // the package name. + if (resolveInfos != null && resolveInfos.size() == 1) { + return getEngineInfo(resolveInfos.get(0), pm); + } + + return null; + } + + /** + * Gets a list of all installed TTS engines. + * + * @return A list of engine info objects. The list can be empty, but never {@code null}. + */ + 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) { + EngineInfo engine = getEngineInfo(resolveInfo, pm); + if (engine != null) { + engines.add(engine); + } + } + Collections.sort(engines, EngineInfoComparator.INSTANCE); + + return engines; + } + + /** + * Checks whether a given engine is enabled or not. Note that all system + * engines are enabled by default. + */ + public boolean isEngineEnabled(String engine) { + // System engines are enabled by default always. + EngineInfo info = getEngineInfo(engine); + if (info != null && info.system) { + return true; + } + + for (String enabled : getUserEnabledEngines()) { + if (engine.equals(enabled)) { + return true; + } + } + return false; + } + + private boolean isSystemEngine(ServiceInfo info) { + final ApplicationInfo appInfo = info.applicationInfo; + return appInfo != null && (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + } + + private EngineInfo getEngineInfo(ResolveInfo resolve, PackageManager pm) { + ServiceInfo service = resolve.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(); + engine.priority = resolve.priority; + engine.system = isSystemEngine(service); + return engine; + } + + return null; + } + + // Note that in addition to this list, all engines that are a part + // of the system are enabled by default. + private String[] getUserEnabledEngines() { + String str = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.TTS_ENABLED_PLUGINS); + if (TextUtils.isEmpty(str)) { + return new String[0]; + } + return str.split(" "); + } + + private static class EngineInfoComparator implements Comparator<EngineInfo> { + private EngineInfoComparator() { } + + static EngineInfoComparator INSTANCE = new EngineInfoComparator(); + + /** + * Engines that are a part of the system image are always lesser + * than those that are not. Within system engines / non system engines + * the engines are sorted in order of their declared priority. + */ + @Override + public int compare(EngineInfo lhs, EngineInfo rhs) { + if (lhs.system && !rhs.system) { + return -1; + } else if (rhs.system && !lhs.system) { + return 1; + } else { + // Either both system engines, or both non system + // engines. + // + // Note, this isn't a typo. Higher priority numbers imply + // higher priority, but are "lower" in the sort order. + return rhs.priority - lhs.priority; + } + } + } + +} diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java index 9309b05..757a8c3 100644 --- a/core/java/android/text/BoringLayout.java +++ b/core/java/android/text/BoringLayout.java @@ -234,18 +234,17 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback * provided Metrics object (or a new one if the provided one was null) * if boring. */ - public static Metrics isBoring(CharSequence text, TextPaint paint, - Metrics metrics) { + public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) { char[] temp = TextUtils.obtain(500); - int len = text.length(); + int length = text.length(); boolean boring = true; outer: - for (int i = 0; i < len; i += 500) { + for (int i = 0; i < length; i += 500) { int j = i + 500; - if (j > len) - j = len; + if (j > length) + j = length; TextUtils.getChars(text, i, j, temp, 0); @@ -265,7 +264,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback if (boring && text instanceof Spanned) { Spanned sp = (Spanned) text; - Object[] styles = sp.getSpans(0, text.length(), ParagraphStyle.class); + Object[] styles = sp.getSpans(0, length, ParagraphStyle.class); if (styles.length > 0) { boring = false; } @@ -278,7 +277,7 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback } TextLine line = TextLine.obtain(); - line.set(paint, text, 0, text.length(), Layout.DIR_LEFT_TO_RIGHT, + line.set(paint, text, 0, length, Layout.DIR_LEFT_TO_RIGHT, Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null); fm.width = (int) FloatMath.ceil(line.metrics(fm)); TextLine.recycle(line); @@ -289,52 +288,63 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback } } - @Override public int getHeight() { + @Override + public int getHeight() { return mBottom; } - @Override public int getLineCount() { + @Override + public int getLineCount() { return 1; } - @Override public int getLineTop(int line) { + @Override + public int getLineTop(int line) { if (line == 0) return 0; else return mBottom; } - @Override public int getLineDescent(int line) { + @Override + public int getLineDescent(int line) { return mDesc; } - @Override public int getLineStart(int line) { + @Override + public int getLineStart(int line) { if (line == 0) return 0; else return getText().length(); } - @Override public int getParagraphDirection(int line) { + @Override + public int getParagraphDirection(int line) { return DIR_LEFT_TO_RIGHT; } - @Override public boolean getLineContainsTab(int line) { + @Override + public boolean getLineContainsTab(int line) { return false; } - @Override public float getLineMax(int line) { + @Override + public float getLineMax(int line) { return mMax; } - @Override public final Directions getLineDirections(int line) { + @Override + public final Directions getLineDirections(int line) { return Layout.DIRS_ALL_LEFT_TO_RIGHT; } + @Override public int getTopPadding() { return mTopPadding; } + @Override public int getBottomPadding() { return mBottomPadding; } diff --git a/core/java/android/text/CharSequenceIterator.java b/core/java/android/text/CharSequenceIterator.java new file mode 100644 index 0000000..4b8ac10 --- /dev/null +++ b/core/java/android/text/CharSequenceIterator.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import java.text.CharacterIterator; + +/** {@hide} */ +public class CharSequenceIterator implements CharacterIterator { + private final CharSequence mValue; + + private final int mLength; + private int mIndex; + + public CharSequenceIterator(CharSequence value) { + mValue = value; + mLength = value.length(); + mIndex = 0; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); + } + } + + /** {@inheritDoc} */ + public char current() { + if (mIndex == mLength) { + return DONE; + } + return mValue.charAt(mIndex); + } + + /** {@inheritDoc} */ + public int getBeginIndex() { + return 0; + } + + /** {@inheritDoc} */ + public int getEndIndex() { + return mLength; + } + + /** {@inheritDoc} */ + public int getIndex() { + return mIndex; + } + + /** {@inheritDoc} */ + public char first() { + return setIndex(0); + } + + /** {@inheritDoc} */ + public char last() { + return setIndex(mLength - 1); + } + + /** {@inheritDoc} */ + public char next() { + if (mIndex == mLength) { + return DONE; + } + return setIndex(mIndex + 1); + } + + /** {@inheritDoc} */ + public char previous() { + if (mIndex == 0) { + return DONE; + } + return setIndex(mIndex - 1); + } + + /** {@inheritDoc} */ + public char setIndex(int index) { + if ((index < 0) || (index > mLength)) { + throw new IllegalArgumentException("Valid range is [" + 0 + "..." + mLength + "]"); + } + mIndex = index; + return current(); + } +} diff --git a/core/java/android/text/GraphicsOperations.java b/core/java/android/text/GraphicsOperations.java index d426d12..831ccc5 100644 --- a/core/java/android/text/GraphicsOperations.java +++ b/core/java/android/text/GraphicsOperations.java @@ -58,6 +58,13 @@ extends CharSequence int flags, float[] advances, int advancesIndex, Paint paint); /** + * Just like {@link Paint#getTextRunAdvances}. + * @hide + */ + float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, + int flags, float[] advances, int advancesIndex, Paint paint, int reserved); + + /** * Just like {@link Paint#getTextRunCursor}. * @hide */ diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java index 761c271..a81be09 100644 --- a/core/java/android/text/MeasuredText.java +++ b/core/java/android/text/MeasuredText.java @@ -16,14 +16,14 @@ package android.text; -import com.android.internal.util.ArrayUtils; - import android.graphics.Canvas; import android.graphics.Paint; import android.text.style.MetricAffectingSpan; import android.text.style.ReplacementSpan; import android.util.Log; +import com.android.internal.util.ArrayUtils; + /** * @hide */ @@ -187,6 +187,7 @@ class MeasuredText { w[mPos] = wid; for (int i = mPos + 1, e = mPos + len; i < e; i++) w[i] = 0; + mPos += len; } if (fm != null) { diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java index 13cb5e6..679e2cc 100644 --- a/core/java/android/text/Selection.java +++ b/core/java/android/text/Selection.java @@ -16,6 +16,8 @@ package android.text; +import java.text.BreakIterator; + /** * Utility class for manipulating cursors and selections in CharSequences. @@ -38,7 +40,7 @@ public class Selection { else return -1; } - + /** * Return the offset of the selection edge or cursor, or -1 if * there is no selection or cursor. @@ -57,7 +59,7 @@ public class Selection { // private static int pin(int value, int min, int max) { // return value < min ? 0 : (value > max ? max : value); // } - + /** * Set the selection anchor to <code>start</code> and the selection edge * to <code>stop</code>. @@ -69,7 +71,7 @@ public class Selection { int ostart = getSelectionStart(text); int oend = getSelectionEnd(text); - + if (ostart != start || oend != stop) { text.setSpan(SELECTION_START, start, start, Spanned.SPAN_POINT_POINT|Spanned.SPAN_INTERMEDIATE); @@ -357,6 +359,42 @@ public class Selection { return true; } + /** {@hide} */ + public static interface PositionIterator { + public static final int DONE = BreakIterator.DONE; + + public int preceding(int position); + public int following(int position); + } + + /** {@hide} */ + public static boolean moveToPreceding( + Spannable text, PositionIterator iter, boolean extendSelection) { + final int offset = iter.preceding(getSelectionEnd(text)); + if (offset != PositionIterator.DONE) { + if (extendSelection) { + extendSelection(text, offset); + } else { + setSelection(text, offset); + } + } + return true; + } + + /** {@hide} */ + public static boolean moveToFollowing( + Spannable text, PositionIterator iter, boolean extendSelection) { + final int offset = iter.following(getSelectionEnd(text)); + if (offset != PositionIterator.DONE) { + if (extendSelection) { + extendSelection(text, offset); + } else { + setSelection(text, offset); + } + } + return true; + } + private static int findEdge(Spannable text, Layout layout, int dir) { int pt = getSelectionEnd(text); int line = layout.getLineForOffset(pt); @@ -419,7 +457,7 @@ public class Selection { private static final class START implements NoCopySpan { } private static final class END implements NoCopySpan { } - + /* * Public constants */ diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index ea5cdfe..6bde802 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -20,6 +20,7 @@ import com.android.internal.util.ArrayUtils; import android.graphics.Canvas; import android.graphics.Paint; +import android.text.style.SuggestionSpan; import java.lang.reflect.Array; @@ -277,8 +278,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, TextWatcher[] recipients = null; if (notify) - recipients = sendTextWillChange(start, end - start, - tbend - tbstart); + recipients = sendTextWillChange(start, end - start, tbend - tbstart); for (int i = mSpanCount - 1; i >= 0; i--) { if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) { @@ -353,6 +353,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, // no need for span fixup on pure insertion if (tbend > tbstart && end - start == 0) { if (notify) { + removeSuggestionSpans(start, end); sendTextChange(recipients, start, end - start, tbend - tbstart); sendTextHasChanged(recipients); } @@ -384,20 +385,10 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } // remove 0-length SPAN_EXCLUSIVE_EXCLUSIVE - // XXX send notification on removal - if (mSpanEnds[i] < mSpanStarts[i]) { - System.arraycopy(mSpans, i + 1, - mSpans, i, mSpanCount - (i + 1)); - System.arraycopy(mSpanStarts, i + 1, - mSpanStarts, i, mSpanCount - (i + 1)); - System.arraycopy(mSpanEnds, i + 1, - mSpanEnds, i, mSpanCount - (i + 1)); - System.arraycopy(mSpanFlags, i + 1, - mSpanFlags, i, mSpanCount - (i + 1)); - - mSpanCount--; + removeSpan(i); } + removeSuggestionSpans(start, end); } if (notify) { @@ -408,6 +399,32 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, return ret; } + /** + * Removes the SuggestionSpan that overlap the [start, end] range, and that would + * not make sense anymore after the change. + */ + private void removeSuggestionSpans(int start, int end) { + for (int i = mSpanCount - 1; i >= 0; i--) { + final int spanEnd = mSpanEnds[i]; + final int spanSpart = mSpanStarts[i]; + if ((mSpans[i] instanceof SuggestionSpan) && ( + (spanSpart < start && spanEnd > start) || + (spanSpart < end && spanEnd > end))) { + removeSpan(i); + } + } + } + + private void removeSpan(int i) { + // XXX send notification on removal + System.arraycopy(mSpans, i + 1, mSpans, i, mSpanCount - (i + 1)); + System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, mSpanCount - (i + 1)); + System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, mSpanCount - (i + 1)); + System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, mSpanCount - (i + 1)); + + mSpanCount--; + } + // Documentation from interface public SpannableStringBuilder replace(int start, int end, CharSequence tb) { return replace(start, end, tb, 0, tb.length()); @@ -465,16 +482,15 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, mGapStart++; mGapLength--; - if (mGapLength < 1) + if (mGapLength < 1) { new Exception("mGapLength < 1").printStackTrace(); + } int oldlen = (end + 1) - start; - int inserted = change(false, start + 1, start + 1, - tb, tbstart, tbend); + int inserted = change(false, start + 1, start + 1, tb, tbstart, tbend); change(false, start, start + 1, "", 0, 0); - change(false, start + inserted, start + inserted + oldlen - 1, - "", 0, 0); + change(false, start + inserted, start + inserted + oldlen - 1, "", 0, 0); /* * Special case to keep the cursor in the same position @@ -1170,6 +1186,35 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } /** + * Don't call this yourself -- exists for Paint to use internally. + * {@hide} + */ + public float getTextRunAdvances(int start, int end, int contextStart, int contextEnd, int flags, + float[] advances, int advancesPos, Paint p, int reserved) { + + float ret; + + int contextLen = contextEnd - contextStart; + int len = end - start; + + if (end <= mGapStart) { + ret = p.getTextRunAdvances(mText, start, len, contextStart, contextLen, + flags, advances, advancesPos, reserved); + } else if (start >= mGapStart) { + ret = p.getTextRunAdvances(mText, start + mGapLength, len, + contextStart + mGapLength, contextLen, flags, advances, advancesPos, reserved); + } else { + char[] buf = TextUtils.obtain(contextLen); + getChars(contextStart, contextEnd, buf, 0); + ret = p.getTextRunAdvances(buf, start - contextStart, len, + 0, contextLen, flags, advances, advancesPos, reserved); + TextUtils.recycle(buf); + } + + return ret; + } + + /** * Returns the next cursor position in the run. This avoids placing the cursor between * surrogates, between characters that form conjuncts, between base characters and combining * marks, or within a reordering cluster. @@ -1245,7 +1290,6 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, private int[] mSpanFlags; private int mSpanCount; - private static final int MARK = 1; private static final int POINT = 2; private static final int PARAGRAPH = 3; diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index a826a97..9e48eff 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -235,6 +235,8 @@ public class StaticLayout extends Layout { } else { MetricAffectingSpan[] spans = spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, spanned, + MetricAffectingSpan.class); measured.addStyleRun(paint, spans, spanLen, fm); } } diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 155870a..f5249611 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -43,6 +43,8 @@ import android.util.Log; * @hide */ class TextLine { + private static final boolean DEBUG = false; + private TextPaint mPaint; private CharSequence mText; private int mStart; @@ -56,7 +58,7 @@ class TextLine { private Spanned mSpanned; private final TextPaint mWorkPaint = new TextPaint(); - private static TextLine[] cached = new TextLine[3]; + private static final TextLine[] sCached = new TextLine[3]; /** * Returns a new TextLine from the shared pool. @@ -65,16 +67,19 @@ class TextLine { */ static TextLine obtain() { TextLine tl; - synchronized (cached) { - for (int i = cached.length; --i >= 0;) { - if (cached[i] != null) { - tl = cached[i]; - cached[i] = null; + synchronized (sCached) { + for (int i = sCached.length; --i >= 0;) { + if (sCached[i] != null) { + tl = sCached[i]; + sCached[i] = null; return tl; } } } tl = new TextLine(); + if (DEBUG) { + Log.v("TLINE", "new: " + tl); + } return tl; } @@ -89,10 +94,10 @@ class TextLine { tl.mText = null; tl.mPaint = null; tl.mDirections = null; - synchronized(cached) { - for (int i = 0; i < cached.length; ++i) { - if (cached[i] == null) { - cached[i] = tl; + synchronized(sCached) { + for (int i = 0; i < sCached.length; ++i) { + if (sCached[i] == null) { + sCached[i] = tl; break; } } @@ -126,12 +131,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) { @@ -146,10 +151,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 @@ -173,11 +179,11 @@ class TextLine { void draw(Canvas c, float x, int top, int y, int bottom) { if (!mHasTabs) { if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { - drawRun(c, 0, 0, mLen, false, x, top, y, bottom, false); + drawRun(c, 0, mLen, false, x, top, y, bottom, false); return; } if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { - drawRun(c, 0, 0, mLen, true, x, top, y, bottom, false); + drawRun(c, 0, mLen, true, x, top, y, bottom, false); return; } } @@ -196,7 +202,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; @@ -215,7 +220,7 @@ class TextLine { } if (j == runLimit || codept == '\t' || bm != null) { - h += drawRun(c, i, segstart, j, runIsRtl, x+h, top, y, bottom, + h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom, i != lastRunIndex || j != mLen); if (codept == '\t') { @@ -274,10 +279,10 @@ class TextLine { if (!mHasTabs) { if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { - return measureRun(0, 0, offset, mLen, false, fmi); + return measureRun(0, offset, mLen, false, fmi); } if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { - return measureRun(0, 0, offset, mLen, true, fmi); + return measureRun(0, offset, mLen, true, fmi); } } @@ -314,14 +319,14 @@ class TextLine { boolean advance = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; if (inSegment && advance) { - return h += measureRun(i, segstart, offset, j, runIsRtl, fmi); + return h += measureRun(segstart, offset, j, runIsRtl, fmi); } - float w = measureRun(i, segstart, j, j, runIsRtl, fmi); + float w = measureRun(segstart, j, j, runIsRtl, fmi); h += advance ? w : -w; if (inSegment) { - return h += measureRun(i, segstart, offset, j, runIsRtl, null); + return h += measureRun(segstart, offset, j, runIsRtl, null); } if (codept == '\t') { @@ -352,8 +357,8 @@ class TextLine { /** * Draws a unidirectional (but possibly multi-styled) run of text. * + * * @param c the canvas to draw on - * @param runIndex the index of this directional run * @param start the line-relative start * @param limit the line-relative limit * @param runIsRtl true if the run is right-to-left @@ -365,25 +370,25 @@ class TextLine { * @return the signed width of the run, based on the paragraph direction. * Only valid if needWidth is true. */ - private float drawRun(Canvas c, int runIndex, int start, + private float drawRun(Canvas c, int start, int limit, boolean runIsRtl, float x, int top, int y, int bottom, boolean needWidth) { if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { - float w = -measureRun(runIndex, start, limit, limit, runIsRtl, null); - handleRun(runIndex, start, limit, limit, runIsRtl, c, x + w, top, + float w = -measureRun(start, limit, limit, runIsRtl, null); + handleRun(start, limit, limit, runIsRtl, c, x + w, top, y, bottom, null, false); return w; } - return handleRun(runIndex, start, limit, limit, runIsRtl, c, x, top, + return handleRun(start, limit, limit, runIsRtl, c, x, top, y, bottom, null, needWidth); } /** * Measures a unidirectional (but possibly multi-styled) run of text. * - * @param runIndex the run index + * * @param start the line-relative start of the run * @param offset the offset to measure to, between start and limit inclusive * @param limit the line-relative limit of the run @@ -393,10 +398,9 @@ class TextLine { * @return the signed width from the start of the run to the leading edge * of the character at offset, based on the run (not paragraph) direction */ - private float measureRun(int runIndex, int start, - int offset, int limit, boolean runIsRtl, FontMetricsInt fmi) { - return handleRun(runIndex, start, offset, limit, runIsRtl, null, - 0, 0, 0, 0, fmi, true); + private float measureRun(int start, int offset, int limit, boolean runIsRtl, + FontMetricsInt fmi) { + return handleRun(start, offset, limit, runIsRtl, null, 0, 0, 0, 0, fmi, true); } /** @@ -628,6 +632,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; @@ -749,9 +754,9 @@ class TextLine { /** * Utility function for measuring and rendering a replacement. * + * * @param replacement the replacement * @param wp the work paint - * @param runIndex the run index * @param start the start of the run * @param limit the limit of the run * @param runIsRtl true if the run is right-to-left @@ -766,7 +771,7 @@ class TextLine { * valid if needWidth is true */ private float handleReplacement(ReplacementSpan replacement, TextPaint wp, - int runIndex, int start, int limit, boolean runIsRtl, Canvas c, + int start, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth) { @@ -794,7 +799,7 @@ class TextLine { * Utility function for handling a unidirectional run. The run must not * contain tabs or emoji but can contain styles. * - * @param runIndex the run index + * * @param start the line-relative start of the run * @param measureLimit the offset to measure to, between start and limit inclusive * @param limit the limit of the run @@ -809,10 +814,17 @@ class TextLine { * @return the signed width of the run based on the run direction; only * valid if needWidth is true */ - private float handleRun(int runIndex, int start, int measureLimit, + private float handleRun(int start, int measureLimit, int limit, boolean runIsRtl, Canvas c, float x, int top, int y, int bottom, FontMetricsInt fmi, boolean needWidth) { + // Case of an empty line, make sure we update fmi according to mPaint + if (start == measureLimit) { + TextPaint wp = mWorkPaint; + wp.set(mPaint); + return handleText(wp, 0, 0, 0, 0, runIsRtl, c, x, top, y, bottom, fmi, needWidth); + } + // Shaping needs to take into account context up to metric boundaries, // but rendering needs to take into account character style boundaries. // So we iterate through metric runs to get metric bounds, @@ -834,6 +846,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; @@ -849,7 +862,7 @@ class TextLine { } if (replacement != null) { - x += handleReplacement(replacement, wp, runIndex, i, + x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y, bottom, fmi, needWidth || mlimit < measureLimit); continue; @@ -867,6 +880,7 @@ class TextLine { CharacterStyle[] spans = mSpanned.getSpans(mStart + j, mStart + jnext, CharacterStyle.class); + spans = TextUtils.removeEmptySpans(spans, mSpanned, CharacterStyle.class); wp.set(mPaint); for (int k = 0; k < spans.length; k++) { diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index cdb7228..6741059 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -37,6 +37,7 @@ import android.text.style.ScaleXSpan; import android.text.style.StrikethroughSpan; import android.text.style.StyleSpan; import android.text.style.SubscriptSpan; +import android.text.style.SuggestionSpan; import android.text.style.SuperscriptSpan; import android.text.style.TextAppearanceSpan; import android.text.style.TypefaceSpan; @@ -44,6 +45,7 @@ import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Printer; +import java.lang.reflect.Array; import java.util.Iterator; import java.util.regex.Pattern; @@ -54,7 +56,7 @@ public class TextUtils { public static void getChars(CharSequence s, int start, int end, char[] dest, int destoff) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (c == String.class) ((String) s).getChars(start, end, dest, destoff); @@ -75,7 +77,7 @@ public class TextUtils { } public static int indexOf(CharSequence s, char ch, int start) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (c == String.class) return ((String) s).indexOf(ch, start); @@ -84,7 +86,7 @@ public class TextUtils { } public static int indexOf(CharSequence s, char ch, int start, int end) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (s instanceof GetChars || c == StringBuffer.class || c == StringBuilder.class || c == String.class) { @@ -125,7 +127,7 @@ public class TextUtils { } public static int lastIndexOf(CharSequence s, char ch, int last) { - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (c == String.class) return ((String) s).lastIndexOf(ch, last); @@ -142,7 +144,7 @@ public class TextUtils { int end = last + 1; - Class c = s.getClass(); + Class<? extends CharSequence> c = s.getClass(); if (s instanceof GetChars || c == StringBuffer.class || c == StringBuilder.class || c == String.class) { @@ -499,6 +501,7 @@ public class TextUtils { return new String(buf); } + @Override public String toString() { return subSequence(0, length()).toString(); } @@ -563,6 +566,8 @@ public class TextUtils { public static final int TEXT_APPEARANCE_SPAN = 17; /** @hide */ public static final int ANNOTATION = 18; + /** @hide */ + public static final int SUGGESTION_SPAN = 19; /** * Flatten a CharSequence and whatever styles can be copied across processes @@ -621,7 +626,7 @@ public class TextUtils { * Read and return a new CharSequence, possibly with styles, * from the parcel. */ - public CharSequence createFromParcel(Parcel p) { + public CharSequence createFromParcel(Parcel p) { int kind = p.readInt(); String string = p.readString(); @@ -714,6 +719,10 @@ public class TextUtils { readSpan(p, sp, new Annotation(p)); break; + case SUGGESTION_SPAN: + readSpan(p, sp, new SuggestionSpan(p)); + break; + default: throw new RuntimeException("bogus span encoding " + kind); } @@ -766,7 +775,7 @@ public class TextUtils { if (where >= 0) tb.setSpan(sources[i], where, where + sources[i].length(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } for (int i = 0; i < sources.length; i++) { @@ -1120,7 +1129,6 @@ public class TextUtils { int remaining = commaCount + 1; int ok = 0; - int okRemaining = remaining; String okFormat = ""; int w = 0; @@ -1152,7 +1160,6 @@ public class TextUtils { if (w + moreWid <= avail) { ok = i + 1; - okRemaining = remaining; okFormat = format; } } @@ -1185,6 +1192,7 @@ public class TextUtils { MetricAffectingSpan.class); MetricAffectingSpan[] spans = sp.getSpans( spanStart, spanEnd, MetricAffectingSpan.class); + spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class); width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null); } } @@ -1543,6 +1551,56 @@ public class TextUtils { return false; } + /** + * Removes empty spans from the <code>spans</code> array. + * + * When parsing a Spanned using {@link Spanned#nextSpanTransition(int, int, Class)}, empty spans + * will (correctly) create span transitions, and calling getSpans on a slice of text bounded by + * one of these transitions will (correctly) include the empty overlapping span. + * + * However, these empty spans should not be taken into account when layouting or rendering the + * string and this method provides a way to filter getSpans' results accordingly. + * + * @param spans A list of spans retrieved using {@link Spanned#getSpans(int, int, Class)} from + * the <code>spanned</code> + * @param spanned The Spanned from which spans were extracted + * @return A subset of spans where empty spans ({@link Spanned#getSpanStart(Object)} == + * {@link Spanned#getSpanEnd(Object)} have been removed. The initial order is preserved + * @hide + */ + @SuppressWarnings("unchecked") + public static <T> T[] removeEmptySpans(T[] spans, Spanned spanned, Class<T> klass) { + T[] copy = null; + int count = 0; + + for (int i = 0; i < spans.length; i++) { + final T span = spans[i]; + final int start = spanned.getSpanStart(span); + final int end = spanned.getSpanEnd(span); + + if (start == end) { + if (copy == null) { + copy = (T[]) Array.newInstance(klass, spans.length - 1); + System.arraycopy(spans, 0, copy, 0, i); + count = i; + } + } else { + if (copy != null) { + copy[count] = span; + count++; + } + } + } + + if (copy != null) { + T[] result = (T[]) Array.newInstance(klass, count); + System.arraycopy(copy, 0, result, 0, count); + return result; + } else { + return spans; + } + } + private static Object sLock = new Object(); private static char[] sTemp = null; } diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index a61ff13..fe96565 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) { @@ -219,8 +234,8 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme if (action == MotionEvent.ACTION_DOWN) { boolean cap = isSelecting(buffer); if (cap) { - int offset = widget.getOffset((int) event.getX(), (int) event.getY()); - + int offset = widget.getOffsetForPosition(event.getX(), event.getY()); + buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT); // Disallow intercepting of the touch events, so that @@ -244,7 +259,7 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme // Update selection as we're moving the selection area. // Get the current touch position - int offset = widget.getOffset((int) event.getX(), (int) event.getY()); + int offset = widget.getOffsetForPosition(event.getX(), event.getY()); Selection.extendSelection(buffer, offset); return true; @@ -260,7 +275,7 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme return true; } - int offset = widget.getOffset((int) event.getX(), (int) event.getY()); + int offset = widget.getOffsetForPosition(event.getX(), event.getY()); if (isSelecting(buffer)) { buffer.removeSpan(LAST_TAP_DOWN); Selection.extendSelection(buffer, offset); @@ -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..af524ee --- /dev/null +++ b/core/java/android/text/method/WordIterator.java @@ -0,0 +1,221 @@ + +/* + * 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 = "Invalid offset: " + offset + + ". 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..240ad9b --- /dev/null +++ b/core/java/android/text/style/SuggestionSpan.java @@ -0,0 +1,198 @@ +/* + * 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.os.SystemClock; +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; + + public static final String ACTION_SUGGESTION_PICKED = "android.text.style.SUGGESTION_PICKED"; + public static final String SUGGESTION_SPAN_PICKED_AFTER = "after"; + public static final String SUGGESTION_SPAN_PICKED_BEFORE = "before"; + public static final String SUGGESTION_SPAN_PICKED_HASHCODE = "hashcode"; + + public 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 mNotificationTargetClassName; + private final int mHashCode; + + /* + * 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 notificationTargetClass if not null, this class will get notified when the user + * selects one of the suggestions. + */ + public SuggestionSpan(Context context, Locale locale, String[] suggestions, int flags, + Class<?> notificationTargetClass) { + 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(); + } + if (notificationTargetClass != null) { + mNotificationTargetClassName = notificationTargetClass.getCanonicalName(); + } else { + mNotificationTargetClassName = ""; + } + mHashCode = hashCodeInternal( + mFlags, mSuggestions, mLocaleString, mNotificationTargetClassName); + } + + public SuggestionSpan(Parcel src) { + mSuggestions = src.readStringArray(); + mFlags = src.readInt(); + mLocaleString = src.readString(); + mNotificationTargetClassName = src.readString(); + mHashCode = src.readInt(); + } + + /** + * @return suggestions + */ + public String[] getSuggestions() { + return mSuggestions; + } + + /** + * @return locale of suggestions + */ + public String getLocale() { + return mLocaleString; + } + + /** + * @return The name of the class to notify. The class of the original IME package will receive + * a notification when the user selects one of the suggestions. The notification will include + * the original string, the suggested replacement string as well as the hashCode of this span. + * The class will get notified by an intent that has those information. + * This is an internal API because only the framework should know the class name. + * + * @hide + */ + public String getNotificationTargetClassName() { + return mNotificationTargetClassName; + } + + 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(mNotificationTargetClassName); + dest.writeInt(mHashCode); + } + + @Override + public int getSpanTypeId() { + return TextUtils.SUGGESTION_SPAN; + } + + @Override + public boolean equals(Object o) { + if (o instanceof SuggestionSpan) { + return ((SuggestionSpan)o).hashCode() == mHashCode; + } + return false; + } + + @Override + public int hashCode() { + return mHashCode; + } + + private static int hashCodeInternal(int flags, String[] suggestions,String locale, + String notificationTargetClassName) { + return Arrays.hashCode(new Object[] {SystemClock.uptimeMillis(), flags, suggestions, locale, + notificationTargetClassName}); + } + + public static final Parcelable.Creator<SuggestionSpan> CREATOR = + new Parcelable.Creator<SuggestionSpan>() { + @Override + public SuggestionSpan createFromParcel(Parcel source) { + return new SuggestionSpan(source); + } + + @Override + public SuggestionSpan[] newArray(int size) { + return new SuggestionSpan[size]; + } + }; +} diff --git a/core/java/android/text/style/TextAppearanceSpan.java b/core/java/android/text/style/TextAppearanceSpan.java index de929e3..deed713 100644 --- a/core/java/android/text/style/TextAppearanceSpan.java +++ b/core/java/android/text/style/TextAppearanceSpan.java @@ -51,10 +51,9 @@ public class TextAppearanceSpan extends MetricAffectingSpan implements Parcelabl * to determine the color. The <code>appearance</code> should be, * for example, <code>android.R.style.TextAppearance_Small</code>, * and the <code>colorList</code> should be, for example, - * <code>android.R.styleable.Theme_textColorDim</code>. + * <code>android.R.styleable.Theme_textColorPrimary</code>. */ - public TextAppearanceSpan(Context context, int appearance, - int colorList) { + public TextAppearanceSpan(Context context, int appearance, int colorList) { ColorStateList textColor; TypedArray a = diff --git a/core/java/android/util/CalendarUtils.java b/core/java/android/util/CalendarUtils.java deleted file mode 100644 index b2b4897..0000000 --- a/core/java/android/util/CalendarUtils.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * 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.util; - -import android.content.AsyncQueryHandler; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.provider.Calendar.CalendarCache; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.text.format.Time; - -import java.util.Formatter; -import java.util.HashSet; -import java.util.Locale; - -/** - * A class containing utility methods related to Calendar apps. - * - * @hide - */ -public class CalendarUtils { - private static final boolean DEBUG = false; - private static final String TAG = "CalendarUtils"; - - /** - * This class contains methods specific to reading and writing time zone - * values. - */ - public static class TimeZoneUtils { - private static final String[] TIMEZONE_TYPE_ARGS = { CalendarCache.TIMEZONE_KEY_TYPE }; - private static final String[] TIMEZONE_INSTANCES_ARGS = - { CalendarCache.TIMEZONE_KEY_INSTANCES }; - - private static StringBuilder mSB = new StringBuilder(50); - private static Formatter mF = new Formatter(mSB, Locale.getDefault()); - private volatile static boolean mFirstTZRequest = true; - private volatile static boolean mTZQueryInProgress = false; - - private volatile static boolean mUseHomeTZ = false; - private volatile static String mHomeTZ = Time.getCurrentTimezone(); - - private static HashSet<Runnable> mTZCallbacks = new HashSet<Runnable>(); - private static int mToken = 1; - private static AsyncTZHandler mHandler; - - // The name of the shared preferences file. This name must be maintained for historical - // reasons, as it's what PreferenceManager assigned the first time the file was created. - private final String mPrefsName; - - /** - * This is the key used for writing whether or not a home time zone should - * be used in the Calendar app to the Calendar Preferences. - */ - public static final String KEY_HOME_TZ_ENABLED = "preferences_home_tz_enabled"; - /** - * This is the key used for writing the time zone that should be used if - * home time zones are enabled for the Calendar app. - */ - public static final String KEY_HOME_TZ = "preferences_home_tz"; - - /** - * This is a helper class for handling the async queries and updates for the - * time zone settings in Calendar. - */ - private class AsyncTZHandler extends AsyncQueryHandler { - public AsyncTZHandler(ContentResolver cr) { - super(cr); - } - - @Override - protected void onQueryComplete(int token, Object cookie, Cursor cursor) { - synchronized (mTZCallbacks) { - if (cursor == null) { - mTZQueryInProgress = false; - mFirstTZRequest = true; - return; - } - - boolean writePrefs = false; - // Check the values in the db - int keyColumn = cursor.getColumnIndexOrThrow(CalendarCache.KEY); - int valueColumn = cursor.getColumnIndexOrThrow(CalendarCache.VALUE); - while(cursor.moveToNext()) { - String key = cursor.getString(keyColumn); - String value = cursor.getString(valueColumn); - if (TextUtils.equals(key, CalendarCache.TIMEZONE_KEY_TYPE)) { - boolean useHomeTZ = !TextUtils.equals( - value, CalendarCache.TIMEZONE_TYPE_AUTO); - if (useHomeTZ != mUseHomeTZ) { - writePrefs = true; - mUseHomeTZ = useHomeTZ; - } - } else if (TextUtils.equals( - key, CalendarCache.TIMEZONE_KEY_INSTANCES_PREVIOUS)) { - if (!TextUtils.isEmpty(value) && !TextUtils.equals(mHomeTZ, value)) { - writePrefs = true; - mHomeTZ = value; - } - } - } - cursor.close(); - if (writePrefs) { - SharedPreferences prefs = getSharedPreferences((Context)cookie, mPrefsName); - // Write the prefs - setSharedPreference(prefs, KEY_HOME_TZ_ENABLED, mUseHomeTZ); - setSharedPreference(prefs, KEY_HOME_TZ, mHomeTZ); - } - - mTZQueryInProgress = false; - for (Runnable callback : mTZCallbacks) { - if (callback != null) { - callback.run(); - } - } - mTZCallbacks.clear(); - } - } - } - - /** - * The name of the file where the shared prefs for Calendar are stored - * must be provided. All activities within an app should provide the - * same preferences name or behavior may become erratic. - * - * @param prefsName - */ - public TimeZoneUtils(String prefsName) { - mPrefsName = prefsName; - } - - /** - * Formats a date or a time range according to the local conventions. - * - * This formats a date/time range using Calendar's time zone and the - * local conventions for the region of the device. - * - * If the {@link DateUtils#FORMAT_UTC} flag is used it will pass in - * the UTC time zone instead. - * - * @param context the context is required only if the time is shown - * @param startMillis the start time in UTC milliseconds - * @param endMillis the end time in UTC milliseconds - * @param flags a bit mask of options See - * {@link DateUtils#formatDateRange(Context, Formatter, long, long, int, String) formatDateRange} - * @return a string containing the formatted date/time range. - */ - public String formatDateRange(Context context, long startMillis, - long endMillis, int flags) { - String date; - String tz; - if ((flags & DateUtils.FORMAT_UTC) != 0) { - tz = Time.TIMEZONE_UTC; - } else { - tz = getTimeZone(context, null); - } - synchronized (mSB) { - mSB.setLength(0); - date = DateUtils.formatDateRange(context, mF, startMillis, endMillis, flags, - tz).toString(); - } - return date; - } - - /** - * Writes a new home time zone to the db. - * - * Updates the home time zone in the db asynchronously and updates - * the local cache. Sending a time zone of - * {@link CalendarCache#TIMEZONE_TYPE_AUTO} will cause it to be set - * to the device's time zone. null or empty tz will be ignored. - * - * @param context The calling activity - * @param timeZone The time zone to set Calendar to, or - * {@link CalendarCache#TIMEZONE_TYPE_AUTO} - */ - public void setTimeZone(Context context, String timeZone) { - if (TextUtils.isEmpty(timeZone)) { - if (DEBUG) { - Log.d(TAG, "Empty time zone, nothing to be done."); - } - return; - } - boolean updatePrefs = false; - synchronized (mTZCallbacks) { - if (CalendarCache.TIMEZONE_TYPE_AUTO.equals(timeZone)) { - if (mUseHomeTZ) { - updatePrefs = true; - } - mUseHomeTZ = false; - } else { - if (!mUseHomeTZ || !TextUtils.equals(mHomeTZ, timeZone)) { - updatePrefs = true; - } - mUseHomeTZ = true; - mHomeTZ = timeZone; - } - } - if (updatePrefs) { - // Write the prefs - SharedPreferences prefs = getSharedPreferences(context, mPrefsName); - setSharedPreference(prefs, KEY_HOME_TZ_ENABLED, mUseHomeTZ); - setSharedPreference(prefs, KEY_HOME_TZ, mHomeTZ); - - // Update the db - ContentValues values = new ContentValues(); - if (mHandler != null) { - mHandler.cancelOperation(mToken); - } - - mHandler = new AsyncTZHandler(context.getContentResolver()); - - // skip 0 so query can use it - if (++mToken == 0) { - mToken = 1; - } - - // Write the use home tz setting - values.put(CalendarCache.VALUE, mUseHomeTZ ? CalendarCache.TIMEZONE_TYPE_HOME - : CalendarCache.TIMEZONE_TYPE_AUTO); - mHandler.startUpdate(mToken, null, CalendarCache.URI, values, CalendarCache.WHERE, - TIMEZONE_TYPE_ARGS); - - // If using a home tz write it to the db - if (mUseHomeTZ) { - ContentValues values2 = new ContentValues(); - values2.put(CalendarCache.VALUE, mHomeTZ); - mHandler.startUpdate(mToken, null, CalendarCache.URI, values2, - CalendarCache.WHERE, TIMEZONE_INSTANCES_ARGS); - } - } - } - - /** - * Gets the time zone that Calendar should be displayed in - * - * This is a helper method to get the appropriate time zone for Calendar. If this - * is the first time this method has been called it will initiate an asynchronous - * query to verify that the data in preferences is correct. The callback supplied - * will only be called if this query returns a value other than what is stored in - * preferences and should cause the calling activity to refresh anything that - * depends on calling this method. - * - * @param context The calling activity - * @param callback The runnable that should execute if a query returns new values - * @return The string value representing the time zone Calendar should display - */ - public String getTimeZone(Context context, Runnable callback) { - synchronized (mTZCallbacks){ - if (mFirstTZRequest) { - mTZQueryInProgress = true; - mFirstTZRequest = false; - - SharedPreferences prefs = getSharedPreferences(context, mPrefsName); - mUseHomeTZ = prefs.getBoolean(KEY_HOME_TZ_ENABLED, false); - mHomeTZ = prefs.getString(KEY_HOME_TZ, Time.getCurrentTimezone()); - - // When the async query returns it should synchronize on - // mTZCallbacks, update mUseHomeTZ, mHomeTZ, and the - // preferences, set mTZQueryInProgress to false, and call all - // the runnables in mTZCallbacks. - if (mHandler == null) { - mHandler = new AsyncTZHandler(context.getContentResolver()); - } - mHandler.startQuery(0, context, CalendarCache.URI, CalendarCache.POJECTION, - null, null, null); - } - if (mTZQueryInProgress) { - mTZCallbacks.add(callback); - } - } - return mUseHomeTZ ? mHomeTZ : Time.getCurrentTimezone(); - } - - /** - * Forces a query of the database to check for changes to the time zone. - * This should be called if another app may have modified the db. If a - * query is already in progress the callback will be added to the list - * of callbacks to be called when it returns. - * - * @param context The calling activity - * @param callback The runnable that should execute if a query returns - * new values - */ - public void forceDBRequery(Context context, Runnable callback) { - synchronized (mTZCallbacks){ - if (mTZQueryInProgress) { - mTZCallbacks.add(callback); - return; - } - mFirstTZRequest = true; - getTimeZone(context, callback); - } - } - } - - /** - * A helper method for writing a String value to the preferences - * asynchronously. - * - * @param context A context with access to the correct preferences - * @param key The preference to write to - * @param value The value to write - */ - public static void setSharedPreference(SharedPreferences prefs, String key, String value) { -// SharedPreferences prefs = getSharedPreferences(context); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(key, value); - editor.apply(); - } - - /** - * A helper method for writing a boolean value to the preferences - * asynchronously. - * - * @param context A context with access to the correct preferences - * @param key The preference to write to - * @param value The value to write - */ - public static void setSharedPreference(SharedPreferences prefs, String key, boolean value) { -// SharedPreferences prefs = getSharedPreferences(context, prefsName); - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(key, value); - editor.apply(); - } - - /** Return a properly configured SharedPreferences instance */ - public static SharedPreferences getSharedPreferences(Context context, String prefsName) { - return context.getSharedPreferences(prefsName, Context.MODE_PRIVATE); - } -} 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/FinitePool.java b/core/java/android/util/FinitePool.java index 3ef8293..4ae21ad 100644 --- a/core/java/android/util/FinitePool.java +++ b/core/java/android/util/FinitePool.java @@ -69,6 +69,7 @@ class FinitePool<T extends Poolable<T>> implements Pool<T> { if (element != null) { element.setNextPoolable(null); + element.setPooled(false); mManager.onAcquired(element); } @@ -76,9 +77,13 @@ class FinitePool<T extends Poolable<T>> implements Pool<T> { } public void release(T element) { + if (element.isPooled()) { + throw new IllegalArgumentException("Element already in the pool."); + } if (mInfinite || mPoolCount < mLimit) { mPoolCount++; element.setNextPoolable(mRoot); + element.setPooled(true); mRoot = element; } mManager.onReleased(element); diff --git a/core/java/android/util/FloatProperty.java b/core/java/android/util/FloatProperty.java new file mode 100644 index 0000000..a67b3cb --- /dev/null +++ b/core/java/android/util/FloatProperty.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.util; + +import android.util.Property; + +/** + * An implementation of {@link android.util.Property} to be used specifically with fields of type + * <code>float</code>. This type-specific subclass enables performance benefit by allowing + * calls to a {@link #set(Object, Float) set()} function that takes the primitive + * <code>float</code> type and avoids autoboxing and other overhead associated with the + * <code>Float</code> class. + * + * @param <T> The class on which the Property is declared. + * + * @hide + */ +public abstract class FloatProperty<T> extends Property<T, Float> { + + public FloatProperty(String name) { + super(Float.class, name); + } + + /** + * A type-specific override of the {@link #set(Object, Float)} that is faster when dealing + * with fields of type <code>float</code>. + */ + public abstract void setValue(T object, float value); + + @Override + final public void set(T object, Float value) { + setValue(object, value); + } + +}
\ No newline at end of file diff --git a/core/java/android/util/IntProperty.java b/core/java/android/util/IntProperty.java new file mode 100644 index 0000000..459d6b2 --- /dev/null +++ b/core/java/android/util/IntProperty.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.util; + +import android.util.Property; + +/** + * An implementation of {@link android.util.Property} to be used specifically with fields of type + * <code>int</code>. This type-specific subclass enables performance benefit by allowing + * calls to a {@link #set(Object, Integer) set()} function that takes the primitive + * <code>int</code> type and avoids autoboxing and other overhead associated with the + * <code>Integer</code> class. + * + * @param <T> The class on which the Property is declared. + * + * @hide + */ +public abstract class IntProperty<T> extends Property<T, Integer> { + + public IntProperty(String name) { + super(Integer.class, name); + } + + /** + * A type-specific override of the {@link #set(Object, Integer)} that is faster when dealing + * with fields of type <code>int</code>. + */ + public abstract void setValue(T object, int value); + + @Override + final public void set(T object, Integer value) { + set(object, value.intValue()); + } + +}
\ No newline at end of file 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/Log.java b/core/java/android/util/Log.java index 38903ab..1c3709f 100644 --- a/core/java/android/util/Log.java +++ b/core/java/android/util/Log.java @@ -20,6 +20,7 @@ import com.android.internal.os.RuntimeInit; import java.io.PrintWriter; import java.io.StringWriter; +import java.net.UnknownHostException; /** * API for sending log output. @@ -302,6 +303,17 @@ public final class Log { if (tr == null) { return ""; } + + // This is to reduce the amount of log spew that apps do in the non-error + // condition of the network being unavailable. + Throwable t = tr; + while (t != null) { + if (t instanceof UnknownHostException) { + return ""; + } + t = t.getCause(); + } + StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); tr.printStackTrace(pw); 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/NoSuchPropertyException.java b/core/java/android/util/NoSuchPropertyException.java new file mode 100644 index 0000000..b93f983 --- /dev/null +++ b/core/java/android/util/NoSuchPropertyException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.util; + +/** + * Thrown when code requests a {@link Property} on a class that does + * not expose the appropriate method or field. + * + * @see Property#of(java.lang.Class, java.lang.Class, java.lang.String) + */ +public class NoSuchPropertyException extends RuntimeException { + + public NoSuchPropertyException(String s) { + super(s); + } + +} diff --git a/core/java/android/util/NtpTrustedTime.java b/core/java/android/util/NtpTrustedTime.java new file mode 100644 index 0000000..5b19ecd --- /dev/null +++ b/core/java/android/util/NtpTrustedTime.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import android.net.SntpClient; +import android.os.SystemClock; + +/** + * {@link TrustedTime} that connects with a remote NTP server as its remote + * trusted time source. + * + * @hide + */ +public class NtpTrustedTime implements TrustedTime { + private String mNtpServer; + private long mNtpTimeout; + + private boolean mHasCache; + private long mCachedNtpTime; + private long mCachedNtpElapsedRealtime; + private long mCachedNtpCertainty; + + public NtpTrustedTime() { + } + + public void setNtpServer(String server, long timeout) { + mNtpServer = server; + mNtpTimeout = timeout; + } + + /** {@inheritDoc} */ + public boolean forceRefresh() { + if (mNtpServer == null) { + // missing server, so no trusted time available + return false; + } + + final SntpClient client = new SntpClient(); + if (client.requestTime(mNtpServer, (int) mNtpTimeout)) { + mHasCache = true; + mCachedNtpTime = client.getNtpTime(); + mCachedNtpElapsedRealtime = client.getNtpTimeReference(); + mCachedNtpCertainty = client.getRoundTripTime() / 2; + return true; + } else { + return false; + } + } + + /** {@inheritDoc} */ + public boolean hasCache() { + return mHasCache; + } + + /** {@inheritDoc} */ + public long getCacheAge() { + if (mHasCache) { + return SystemClock.elapsedRealtime() - mCachedNtpElapsedRealtime; + } else { + return Long.MAX_VALUE; + } + } + + /** {@inheritDoc} */ + public long getCacheCertainty() { + if (mHasCache) { + return mCachedNtpCertainty; + } else { + return Long.MAX_VALUE; + } + } + + /** {@inheritDoc} */ + public long currentTimeMillis() { + if (!mHasCache) { + throw new IllegalStateException("Missing authoritative time source"); + } + + // current time is age after the last ntp cache; callers who + // want fresh values will hit makeAuthoritative() first. + return mCachedNtpTime + getCacheAge(); + } +} diff --git a/core/java/android/util/Poolable.java b/core/java/android/util/Poolable.java index fd9bd9b..87e0529 100644 --- a/core/java/android/util/Poolable.java +++ b/core/java/android/util/Poolable.java @@ -22,4 +22,6 @@ package android.util; public interface Poolable<T> { void setNextPoolable(T element); T getNextPoolable(); + boolean isPooled(); + void setPooled(boolean isPooled); } diff --git a/core/java/android/util/Property.java b/core/java/android/util/Property.java new file mode 100644 index 0000000..146db80 --- /dev/null +++ b/core/java/android/util/Property.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.util; + + +/** + * A property is an abstraction that can be used to represent a <emb>mutable</em> value that is held + * in a <em>host</em> object. The Property's {@link #set(Object, Object)} or {@link #get(Object)} + * methods can be implemented in terms of the private fields of the host object, or via "setter" and + * "getter" methods or by some other mechanism, as appropriate. + * + * @param <T> The class on which the property is declared. + * @param <V> The type that this property represents. + */ +public abstract class Property<T, V> { + + private final String mName; + private final Class<V> mType; + + /** + * This factory method creates and returns a Property given the <code>class</code> and + * <code>name</code> parameters, where the <code>"name"</code> parameter represents either: + * <ul> + * <li>a public <code>getName()</code> method on the class which takes no arguments, plus an + * optional public <code>setName()</code> method which takes a value of the same type + * returned by <code>getName()</code> + * <li>a public <code>isName()</code> method on the class which takes no arguments, plus an + * optional public <code>setName()</code> method which takes a value of the same type + * returned by <code>isName()</code> + * <li>a public <code>name</code> field on the class + * </ul> + * + * <p>If either of the get/is method alternatives is found on the class, but an appropriate + * <code>setName()</code> method is not found, the <code>Property</code> will be + * {@link #isReadOnly() readOnly}. Calling the {@link #set(Object, Object)} method on such + * a property is allowed, but will have no effect.</p> + * + * <p>If neither the methods nor the field are found on the class a + * {@link NoSuchPropertyException} exception will be thrown.</p> + */ + public static <T, V> Property<T, V> of(Class<T> hostType, Class<V> valueType, String name) { + return new ReflectiveProperty<T, V>(hostType, valueType, name); + } + + /** + * A constructor that takes an identifying name and {@link #getType() type} for the property. + */ + public Property(Class<V> type, String name) { + mName = name; + mType = type; + } + + /** + * Returns true if the {@link #set(Object, Object)} method does not set the value on the target + * object (in which case the {@link #set(Object, Object) set()} method should throw a {@link + * NoSuchPropertyException} exception). This may happen if the Property wraps functionality that + * allows querying the underlying value but not setting it. For example, the {@link #of(Class, + * Class, String)} factory method may return a Property with name "foo" for an object that has + * only a <code>getFoo()</code> or <code>isFoo()</code> method, but no matching + * <code>setFoo()</code> method. + */ + public boolean isReadOnly() { + return false; + } + + /** + * Sets the value on <code>object</code> which this property represents. If the method is unable + * to set the value on the target object it will throw an {@link UnsupportedOperationException} + * exception. + */ + public void set(T object, V value) { + throw new UnsupportedOperationException("Property " + getName() +" is read-only"); + } + + /** + * Returns the current value that this property represents on the given <code>object</code>. + */ + public abstract V get(T object); + + /** + * Returns the name for this property. + */ + public String getName() { + return mName; + } + + /** + * Returns the type for this property. + */ + public Class<V> getType() { + return mType; + } +} diff --git a/core/java/android/util/ReflectiveProperty.java b/core/java/android/util/ReflectiveProperty.java new file mode 100644 index 0000000..6832240 --- /dev/null +++ b/core/java/android/util/ReflectiveProperty.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.util; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Internal class to automatically generate a Property for a given class/name pair, given the + * specification of {@link Property#of(java.lang.Class, java.lang.Class, java.lang.String)} + */ +class ReflectiveProperty<T, V> extends Property<T, V> { + + private static final String PREFIX_GET = "get"; + private static final String PREFIX_IS = "is"; + private static final String PREFIX_SET = "set"; + private Method mSetter; + private Method mGetter; + private Field mField; + + /** + * For given property name 'name', look for getName/isName method or 'name' field. + * Also look for setName method (optional - could be readonly). Failing method getters and + * field results in throwing NoSuchPropertyException. + * + * @param propertyHolder The class on which the methods or field are found + * @param name The name of the property, where this name is capitalized and appended to + * "get" and "is to search for the appropriate methods. If the get/is methods are not found, + * the constructor will search for a field with that exact name. + */ + public ReflectiveProperty(Class<T> propertyHolder, Class<V> valueType, String name) { + // TODO: cache reflection info for each new class/name pair + super(valueType, name); + char firstLetter = Character.toUpperCase(name.charAt(0)); + String theRest = name.substring(1); + String capitalizedName = firstLetter + theRest; + String getterName = PREFIX_GET + capitalizedName; + try { + mGetter = propertyHolder.getMethod(getterName, (Class<?>[])null); + } catch (NoSuchMethodException e) { + // getName() not available - try isName() instead + getterName = PREFIX_IS + capitalizedName; + try { + mGetter = propertyHolder.getMethod(getterName, (Class<?>[])null); + } catch (NoSuchMethodException e1) { + // Try public field instead + try { + mField = propertyHolder.getField(name); + Class fieldType = mField.getType(); + if (!typesMatch(valueType, fieldType)) { + throw new NoSuchPropertyException("Underlying type (" + fieldType + ") " + + "does not match Property type (" + valueType + ")"); + } + return; + } catch (NoSuchFieldException e2) { + // no way to access property - throw appropriate exception + throw new NoSuchPropertyException("No accessor method or field found for" + + " property with name " + name); + } + } + } + Class getterType = mGetter.getReturnType(); + // Check to make sure our getter type matches our valueType + if (!typesMatch(valueType, getterType)) { + throw new NoSuchPropertyException("Underlying type (" + getterType + ") " + + "does not match Property type (" + valueType + ")"); + } + String setterName = PREFIX_SET + capitalizedName; + try { + mSetter = propertyHolder.getMethod(setterName, getterType); + } catch (NoSuchMethodException ignored) { + // Okay to not have a setter - just a readonly property + } + } + + /** + * Utility method to check whether the type of the underlying field/method on the target + * object matches the type of the Property. The extra checks for primitive types are because + * generics will force the Property type to be a class, whereas the type of the underlying + * method/field will probably be a primitive type instead. Accept float as matching Float, + * etc. + */ + private boolean typesMatch(Class<V> valueType, Class getterType) { + if (getterType != valueType) { + if (getterType.isPrimitive()) { + return (getterType == float.class && valueType == Float.class) || + (getterType == int.class && valueType == Integer.class) || + (getterType == boolean.class && valueType == Boolean.class) || + (getterType == long.class && valueType == Long.class) || + (getterType == double.class && valueType == Double.class) || + (getterType == short.class && valueType == Short.class) || + (getterType == byte.class && valueType == Byte.class) || + (getterType == char.class && valueType == Character.class); + } + return false; + } + return true; + } + + @Override + public void set(T object, V value) { + if (mSetter != null) { + try { + mSetter.invoke(object, value); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getCause()); + } + } else if (mField != null) { + try { + mField.set(object, value); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } else { + throw new UnsupportedOperationException("Property " + getName() +" is read-only"); + } + } + + @Override + public V get(T object) { + if (mGetter != null) { + try { + return (V) mGetter.invoke(object, (Object[])null); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getCause()); + } + } else if (mField != null) { + try { + return (V) mField.get(object); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + // Should not get here: there should always be a non-null getter or field + throw new AssertionError(); + } + + /** + * Returns false if there is no setter or public field underlying this Property. + */ + @Override + public boolean isReadOnly() { + return (mSetter == null && mField == null); + } +} diff --git a/core/java/android/util/TimeUtils.java b/core/java/android/util/TimeUtils.java index 9042505..93299eb 100644 --- a/core/java/android/util/TimeUtils.java +++ b/core/java/android/util/TimeUtils.java @@ -19,7 +19,7 @@ package android.util; import android.content.res.Resources; import android.content.res.XmlResourceParser; -import org.apache.harmony.luni.internal.util.ZoneInfoDB; +import libcore.util.ZoneInfoDB; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; diff --git a/core/java/android/util/TrustedTime.java b/core/java/android/util/TrustedTime.java new file mode 100644 index 0000000..263d782 --- /dev/null +++ b/core/java/android/util/TrustedTime.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +/** + * Interface that provides trusted time information, possibly coming from an NTP + * server. Implementations may cache answers until {@link #forceRefresh()}. + * + * @hide + */ +public interface TrustedTime { + /** + * Force update with an external trusted time source, returning {@code true} + * when successful. + */ + public boolean forceRefresh(); + + /** + * Check if this instance has cached a response from a trusted time source. + */ + public boolean hasCache(); + + /** + * Return time since last trusted time source contact, or + * {@link Long#MAX_VALUE} if never contacted. + */ + public long getCacheAge(); + + /** + * Return certainty of cached trusted time in milliseconds, or + * {@link Long#MAX_VALUE} if never contacted. Smaller values are more + * precise. + */ + public long getCacheCertainty(); + + /** + * Return current time similar to {@link System#currentTimeMillis()}, + * possibly using a cached authoritative time source. + */ + public long currentTimeMillis(); +} diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java index b0c33e5..041e8a8 100644 --- a/core/java/android/util/Xml.java +++ b/core/java/android/util/Xml.java @@ -16,23 +16,21 @@ package android.util; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import org.apache.harmony.xml.ExpatReader; +import org.kxml2.io.KXmlParser; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlSerializer; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.io.UnsupportedEncodingException; - -import org.apache.harmony.xml.ExpatPullParser; -import org.apache.harmony.xml.ExpatReader; +import org.xmlpull.v1.XmlSerializer; /** * XML utility methods. @@ -46,7 +44,7 @@ public class Xml { * @see <a href="http://xmlpull.org/v1/doc/features.html#relaxed"> * specification</a> */ - public static String FEATURE_RELAXED = ExpatPullParser.FEATURE_RELAXED; + public static String FEATURE_RELAXED = "http://xmlpull.org/v1/doc/features.html#relaxed"; /** * Parses the given xml string and fires events on the given SAX handler. @@ -57,8 +55,7 @@ public class Xml { XMLReader reader = new ExpatReader(); reader.setContentHandler(contentHandler); reader.parse(new InputSource(new StringReader(xml))); - } - catch (IOException e) { + } catch (IOException e) { throw new AssertionError(e); } } @@ -88,16 +85,17 @@ public class Xml { } /** - * Creates a new pull parser with namespace support. - * - * <p><b>Note:</b> This is actually slower than the SAX parser, and it's not - * fully implemented. If you need a fast, mostly implemented pull parser, - * use this. If you need a complete implementation, use KXML. + * Returns a new pull parser with namespace support. */ public static XmlPullParser newPullParser() { - ExpatPullParser parser = new ExpatPullParser(); - parser.setNamespaceProcessingEnabled(true); - return parser; + try { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + return parser; + } catch (XmlPullParserException e) { + throw new AssertionError(); + } } /** diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index 14f2e9d..d5cad96 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -39,6 +39,12 @@ import android.text.TextUtils; * An implementation of Canvas on top of OpenGL ES 2.0. */ class GLES20Canvas extends HardwareCanvas { + // Must match modifiers used in the JNI layer + private static final int MODIFIER_NONE = 0; + private static final int MODIFIER_SHADOW = 1; + private static final int MODIFIER_SHADER = 2; + private static final int MODIFIER_COLOR_FILTER = 4; + private final boolean mOpaque; private int mRenderer; @@ -154,8 +160,10 @@ class GLES20Canvas extends HardwareCanvas { // Hardware layers /////////////////////////////////////////////////////////////////////////// + static native int nCreateTextureLayer(int[] layerInfo); static native int nCreateLayer(int width, int height, boolean isOpaque, int[] layerInfo); static native void nResizeLayer(int layerId, int width, int height, int[] layerInfo); + static native void nUpdateTextureLayer(int layerId, int width, int height, int surface); static native void nDestroyLayer(int layerId); static native void nDestroyLayerDeferred(int layerId); @@ -193,6 +201,14 @@ class GLES20Canvas extends HardwareCanvas { private static native void nSetViewport(int renderer, int width, int height); /** + * Preserves the back buffer of the current surface after a buffer swap. + * Calling this method sets the EGL_SWAP_BEHAVIOR attribute of the current + * surface to EGL_BUFFER_PRESERVED. Calling this method requires an EGL + * config that supports EGL_SWAP_BEHAVIOR_PRESERVED_BIT. + * + * @return True if the swap behavior was successfully changed, + * false otherwise. + * * @hide */ public static boolean preserveBackBuffer() { @@ -200,6 +216,21 @@ class GLES20Canvas extends HardwareCanvas { } private static native boolean nPreserveBackBuffer(); + + /** + * Indicates whether the current surface preserves its back buffer + * after a buffer swap. + * + * @return True, if the surface's EGL_SWAP_BEHAVIOR is EGL_BUFFER_PRESERVED, + * false otherwise + * + * @hide + */ + public static boolean isBackBufferPreserved() { + return nIsBackBufferPreserved(); + } + + private static native boolean nIsBackBufferPreserved(); @Override void onPreDraw(Rect dirty) { @@ -253,20 +284,27 @@ class GLES20Canvas extends HardwareCanvas { private static native boolean nDrawDisplayList(int renderer, int displayList, int width, int height, Rect dirty); + @Override + void outputDisplayList(DisplayList displayList) { + nOutputDisplayList(mRenderer, ((GLES20DisplayList) displayList).mNativeDisplayList); + } + + private static native void nOutputDisplayList(int renderer, int displayList); + /////////////////////////////////////////////////////////////////////////// // Hardware layer /////////////////////////////////////////////////////////////////////////// void drawHardwareLayer(HardwareLayer layer, float x, float y, Paint paint) { final GLES20Layer glLayer = (GLES20Layer) layer; - boolean hasColorFilter = paint != null && setupColorFilter(paint); + int modifier = paint != null ? setupColorFilter(paint) : MODIFIER_NONE; final int nativePaint = paint == null ? 0 : paint.mNativePaint; nDrawLayer(mRenderer, glLayer.getLayer(), x, y, nativePaint); - if (hasColorFilter) nResetModifiers(mRenderer); + if (modifier != MODIFIER_NONE) nResetModifiers(mRenderer, modifier); } private static native void nDrawLayer(int renderer, int layer, float x, float y, int paint); - + void interrupt() { nInterrupt(mRenderer); } @@ -455,10 +493,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 +565,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 +583,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 +596,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 +608,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 +621,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 +638,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 +659,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 +693,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 +706,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 +740,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 +755,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 +772,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 +780,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 +805,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 +835,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 +860,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 +875,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 +888,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 +905,7 @@ class GLES20Canvas extends HardwareCanvas { TemporaryBuffer.recycle(buf); } } finally { - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } @@ -872,11 +915,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 +928,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 +958,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 +977,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 +997,7 @@ class GLES20Canvas extends HardwareCanvas { TemporaryBuffer.recycle(buf); } } finally { - if (hasModifier) nResetModifiers(mRenderer); + if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); } } @@ -968,43 +1011,57 @@ class GLES20Canvas extends HardwareCanvas { // TODO: Implement } - private boolean setupModifiers(Paint paint) { - boolean hasModifier = false; + private int setupModifiers(Bitmap b, Paint paint) { + if (b.getConfig() == Bitmap.Config.ALPHA_8) { + return setupModifiers(paint); + } + + final ColorFilter filter = paint.getColorFilter(); + if (filter != null) { + nSetupColorFilter(mRenderer, filter.nativeColorFilter); + return MODIFIER_COLOR_FILTER; + } + + return MODIFIER_NONE; + } + + private int setupModifiers(Paint paint) { + int modifiers = MODIFIER_NONE; if (paint.hasShadow) { nSetupShadow(mRenderer, paint.shadowRadius, paint.shadowDx, paint.shadowDy, paint.shadowColor); - hasModifier = true; + modifiers |= MODIFIER_SHADOW; } final Shader shader = paint.getShader(); if (shader != null) { nSetupShader(mRenderer, shader.native_shader); - hasModifier = true; + modifiers |= MODIFIER_SHADER; } final ColorFilter filter = paint.getColorFilter(); if (filter != null) { nSetupColorFilter(mRenderer, filter.nativeColorFilter); - hasModifier = true; + modifiers |= MODIFIER_COLOR_FILTER; } - return hasModifier; + return modifiers; } - private boolean setupColorFilter(Paint paint) { + private int setupColorFilter(Paint paint) { final ColorFilter filter = paint.getColorFilter(); if (filter != null) { nSetupColorFilter(mRenderer, filter.nativeColorFilter); - return true; + return MODIFIER_COLOR_FILTER; } - return false; + return MODIFIER_NONE; } - + private static native void nSetupShader(int renderer, int shader); private static native void nSetupColorFilter(int renderer, int colorFilter); private static native void nSetupShadow(int renderer, float radius, float dx, float dy, int color); - private static native void nResetModifiers(int renderer); + private static native void nResetModifiers(int renderer, int modifiers); } diff --git a/core/java/android/view/GLES20Layer.java b/core/java/android/view/GLES20Layer.java index 6000a4a..bc191a6 100644 --- a/core/java/android/view/GLES20Layer.java +++ b/core/java/android/view/GLES20Layer.java @@ -14,39 +14,21 @@ * limitations under the License. */ -package android.view; -import android.graphics.Canvas; +package android.view; /** * An OpenGL ES 2.0 implementation of {@link HardwareLayer}. */ -class GLES20Layer extends HardwareLayer { - private int mLayer; - - private int mLayerWidth; - private int mLayerHeight; - - private final GLES20Canvas mCanvas; +abstract class GLES20Layer extends HardwareLayer { + int mLayer; + Finalizer mFinalizer; - @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) - private final Finalizer mFinalizer; - - GLES20Layer(int width, int height, boolean isOpaque) { - super(width, height, isOpaque); - - int[] layerInfo = new int[2]; - mLayer = GLES20Canvas.nCreateLayer(width, height, isOpaque, layerInfo); - if (mLayer != 0) { - mLayerWidth = layerInfo[0]; - mLayerHeight = layerInfo[1]; + GLES20Layer() { + } - mCanvas = new GLES20Canvas(mLayer, !isOpaque); - mFinalizer = new Finalizer(mLayer); - } else { - mCanvas = null; - mFinalizer = null; - } + GLES20Layer(int width, int height, boolean opaque) { + super(width, height, opaque); } /** @@ -58,55 +40,14 @@ class GLES20Layer extends HardwareLayer { return mLayer; } - @Override - boolean isValid() { - return mLayer != 0 && mLayerWidth > 0 && mLayerHeight > 0; - } - - @Override - void resize(int width, int height) { - if (!isValid() || width <= 0 || height <= 0) return; - - mWidth = width; - mHeight = height; - - if (width != mLayerWidth || height != mLayerHeight) { - int[] layerInfo = new int[2]; - - GLES20Canvas.nResizeLayer(mLayer, width, height, layerInfo); - - mLayerWidth = layerInfo[0]; - mLayerHeight = layerInfo[1]; - } - } - - @Override - HardwareCanvas getCanvas() { - return mCanvas; - } - - @Override - void end(Canvas currentCanvas) { - if (currentCanvas instanceof GLES20Canvas) { - ((GLES20Canvas) currentCanvas).resume(); - } - } - - @Override - HardwareCanvas start(Canvas currentCanvas) { - if (currentCanvas instanceof GLES20Canvas) { - ((GLES20Canvas) currentCanvas).interrupt(); - } - return getCanvas(); - } - + @Override void destroy() { - mFinalizer.destroy(); + if (mFinalizer != null) mFinalizer.destroy(); mLayer = 0; } - private static class Finalizer { + static class Finalizer { private int mLayerId; public Finalizer(int layerId) { diff --git a/core/java/android/view/GLES20RecordingCanvas.java b/core/java/android/view/GLES20RecordingCanvas.java index 2603281..ec94fe7 100644 --- a/core/java/android/view/GLES20RecordingCanvas.java +++ b/core/java/android/view/GLES20RecordingCanvas.java @@ -25,7 +25,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; -import java.util.HashSet; +import java.util.ArrayList; /** * An implementation of a GL canvas that records drawing operations. @@ -37,7 +37,7 @@ class GLES20RecordingCanvas extends GLES20Canvas { // These lists ensure that any Bitmaps recorded by a DisplayList are kept alive as long // as the DisplayList is alive. @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"}) - private final HashSet<Bitmap> mBitmaps = new HashSet<Bitmap>(); + private final ArrayList<Bitmap> mBitmaps = new ArrayList<Bitmap>(5); GLES20RecordingCanvas(boolean translucent) { super(true, translucent); diff --git a/core/java/android/view/GLES20RenderLayer.java b/core/java/android/view/GLES20RenderLayer.java new file mode 100644 index 0000000..7adac1c --- /dev/null +++ b/core/java/android/view/GLES20RenderLayer.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Canvas; + +/** + * An OpenGL ES 2.0 implementation of {@link HardwareLayer}. This + * implementation can be used a rendering target. It generates a + * {@link Canvas} that can be used to render into an FBO using OpenGL. + */ +class GLES20RenderLayer extends GLES20Layer { + + private int mLayerWidth; + private int mLayerHeight; + + private final GLES20Canvas mCanvas; + + GLES20RenderLayer(int width, int height, boolean isOpaque) { + super(width, height, isOpaque); + + int[] layerInfo = new int[2]; + mLayer = GLES20Canvas.nCreateLayer(width, height, isOpaque, layerInfo); + if (mLayer != 0) { + mLayerWidth = layerInfo[0]; + mLayerHeight = layerInfo[1]; + + mCanvas = new GLES20Canvas(mLayer, !isOpaque); + mFinalizer = new Finalizer(mLayer); + } else { + mCanvas = null; + mFinalizer = null; + } + } + + @Override + boolean isValid() { + return mLayer != 0 && mLayerWidth > 0 && mLayerHeight > 0; + } + + @Override + void resize(int width, int height) { + if (!isValid() || width <= 0 || height <= 0) return; + + mWidth = width; + mHeight = height; + + if (width != mLayerWidth || height != mLayerHeight) { + int[] layerInfo = new int[2]; + + GLES20Canvas.nResizeLayer(mLayer, width, height, layerInfo); + + mLayerWidth = layerInfo[0]; + mLayerHeight = layerInfo[1]; + } + } + + @Override + HardwareCanvas getCanvas() { + return mCanvas; + } + + @Override + void end(Canvas currentCanvas) { + if (currentCanvas instanceof GLES20Canvas) { + ((GLES20Canvas) currentCanvas).resume(); + } + } + + @Override + HardwareCanvas start(Canvas currentCanvas) { + if (currentCanvas instanceof GLES20Canvas) { + ((GLES20Canvas) currentCanvas).interrupt(); + } + return getCanvas(); + } +} diff --git a/core/java/android/view/GLES20TextureLayer.java b/core/java/android/view/GLES20TextureLayer.java new file mode 100644 index 0000000..fcf421b --- /dev/null +++ b/core/java/android/view/GLES20TextureLayer.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Canvas; +import android.graphics.SurfaceTexture; + +/** + * An OpenGL ES 2.0 implementation of {@link HardwareLayer}. This + * implementation can be used as a texture. Rendering into this + * layer is not controlled by a {@link HardwareCanvas}. + */ +class GLES20TextureLayer extends GLES20Layer { + private int mTexture; + private SurfaceTexture mSurface; + + GLES20TextureLayer() { + int[] layerInfo = new int[2]; + mLayer = GLES20Canvas.nCreateTextureLayer(layerInfo); + + if (mLayer != 0) { + mTexture = layerInfo[0]; + mFinalizer = new Finalizer(mLayer); + } else { + mFinalizer = null; + } + } + + @Override + boolean isValid() { + return mLayer != 0 && mTexture != 0; + } + + @Override + void resize(int width, int height) { + } + + @Override + HardwareCanvas getCanvas() { + return null; + } + + @Override + HardwareCanvas start(Canvas currentCanvas) { + return null; + } + + @Override + void end(Canvas currentCanvas) { + } + + SurfaceTexture getSurfaceTexture() { + if (mSurface == null) { + mSurface = new SurfaceTexture(mTexture); + } + return mSurface; + } + + void update(int width, int height, int surface) { + GLES20Canvas.nUpdateTextureLayer(mLayer, width, height, surface); + } +} diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index 79b3d42..a496a9e 100755..100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -193,8 +193,10 @@ public class GestureDetector { } } + // TODO: ViewConfiguration + private int mBiggerTouchSlopSquare = 20 * 20; + private int mTouchSlopSquare; - private int mLargeTouchSlopSquare; private int mDoubleTapSlopSquare; private int mMinimumFlingVelocity; private int mMaximumFlingVelocity; @@ -243,6 +245,13 @@ public class GestureDetector { */ private VelocityTracker mVelocityTracker; + /** + * Consistency verifier for debugging purposes. + */ + private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = + InputEventConsistencyVerifier.isInstrumentationEnabled() ? + new InputEventConsistencyVerifier(this, 0) : null; + private class GestureHandler extends Handler { GestureHandler() { super(); @@ -382,11 +391,10 @@ public class GestureDetector { mIgnoreMultitouch = ignoreMultitouch; // Fallback to support pre-donuts releases - int touchSlop, largeTouchSlop, doubleTapSlop; + int touchSlop, doubleTapSlop; if (context == null) { //noinspection deprecation touchSlop = ViewConfiguration.getTouchSlop(); - largeTouchSlop = touchSlop + 2; doubleTapSlop = ViewConfiguration.getDoubleTapSlop(); //noinspection deprecation mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); @@ -394,13 +402,11 @@ public class GestureDetector { } else { final ViewConfiguration configuration = ViewConfiguration.get(context); touchSlop = configuration.getScaledTouchSlop(); - largeTouchSlop = configuration.getScaledLargeTouchSlop(); doubleTapSlop = configuration.getScaledDoubleTapSlop(); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity(); } mTouchSlopSquare = touchSlop * touchSlop; - mLargeTouchSlopSquare = largeTouchSlop * largeTouchSlop; mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; } @@ -444,6 +450,10 @@ public class GestureDetector { * else false. */ public boolean onTouchEvent(MotionEvent ev) { + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTouchEvent(ev, 0); + } + final int action = ev.getAction(); final float y = ev.getY(); final float x = ev.getX(); @@ -535,7 +545,7 @@ public class GestureDetector { mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); } - if (distance > mLargeTouchSlopSquare) { + if (distance > mBiggerTouchSlopSquare) { mAlwaysInBiggerTapRegion = false; } } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { @@ -580,8 +590,14 @@ public class GestureDetector { mHandler.removeMessages(SHOW_PRESS); mHandler.removeMessages(LONG_PRESS); break; + case MotionEvent.ACTION_CANCEL: cancel(); + break; + } + + if (!handled && mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0); } return handled; } @@ -594,6 +610,8 @@ public class GestureDetector { mVelocityTracker = null; mIsDoubleTapping = false; mStillDown = false; + mAlwaysInTapRegion = false; + mAlwaysInBiggerTapRegion = false; if (mInLongPress) { mInLongPress = false; } diff --git a/core/java/android/view/Gravity.java b/core/java/android/view/Gravity.java index cf79638..69e6489 100644 --- a/core/java/android/view/Gravity.java +++ b/core/java/android/view/Gravity.java @@ -80,9 +80,14 @@ public class Gravity /** Flag to clip the edges of the object to its container along the * horizontal axis. */ public static final int CLIP_HORIZONTAL = AXIS_CLIP<<AXIS_X_SHIFT; - + + /** Raw bit controlling whether the layout direction is relative or not (START/END instead of + * absolute LEFT/RIGHT). + */ + public static final int RELATIVE_LAYOUT_DIRECTION = 0x00800000; + /** - * Binary mask to get the horizontal gravity of a gravity. + * Binary mask to get the absolute horizontal gravity of a gravity. */ public static final int HORIZONTAL_GRAVITY_MASK = (AXIS_SPECIFIED | AXIS_PULL_BEFORE | AXIS_PULL_AFTER) << AXIS_X_SHIFT; @@ -106,8 +111,19 @@ public class Gravity */ public static final int DISPLAY_CLIP_HORIZONTAL = 0x01000000; + /** Push object to x-axis position at the start of its container, not changing its size. */ + public static final int START = RELATIVE_LAYOUT_DIRECTION | LEFT; + + /** Push object to x-axis position at the end of its container, not changing its size. */ + public static final int END = RELATIVE_LAYOUT_DIRECTION | RIGHT; + /** - * Apply a gravity constant to an object. + * Binary mask for the horizontal gravity and script specific direction bit. + */ + public static final int RELATIVE_HORIZONTAL_GRAVITY_MASK = START | END; + + /** + * Apply a gravity constant to an object. This suppose that the layout direction is LTR. * * @param gravity The desired placement of the object, as defined by the * constants in this class. @@ -119,12 +135,33 @@ public class Gravity * @param outRect Receives the computed frame of the object in its * container. */ - public static void apply(int gravity, int w, int h, Rect container, - Rect outRect) { + public static void apply(int gravity, int w, int h, Rect container, Rect outRect) { apply(gravity, w, h, container, 0, 0, outRect); } /** + * Apply a gravity constant to an object and take care if layout direction is RTL or not. + * + * @param gravity The desired placement of the object, as defined by the + * constants in this class. + * @param w The horizontal size of the object. + * @param h The vertical size of the object. + * @param container The frame of the containing space, in which the object + * will be placed. Should be large enough to contain the + * width and height of the object. + * @param outRect Receives the computed frame of the object in its + * container. + * @param isRtl Whether the layout is right-to-left. + * + * @hide + */ + public static void apply(int gravity, int w, int h, Rect container, + Rect outRect, boolean isRtl) { + int absGravity = getAbsoluteGravity(gravity, isRtl); + apply(absGravity, w, h, container, 0, 0, outRect); + } + + /** * Apply a gravity constant to an object. * * @param gravity The desired placement of the object, as defined by the @@ -146,7 +183,7 @@ public class Gravity * container. */ public static void apply(int gravity, int w, int h, Rect container, - int xAdj, int yAdj, Rect outRect) { + int xAdj, int yAdj, Rect outRect) { switch (gravity&((AXIS_PULL_BEFORE|AXIS_PULL_AFTER)<<AXIS_X_SHIFT)) { case 0: outRect.left = container.left @@ -301,6 +338,48 @@ public class Gravity * @return true if the supplied gravity has an horizontal pull */ public static boolean isHorizontal(int gravity) { - return gravity > 0 && (gravity & HORIZONTAL_GRAVITY_MASK) != 0; + return gravity > 0 && (gravity & RELATIVE_HORIZONTAL_GRAVITY_MASK) != 0; + } + + /** + * <p>Convert script specific gravity to absolute horizontal value.</p> + * + * if horizontal direction is LTR, then START will set LEFT and END will set RIGHT. + * if horizontal direction is RTL, then START will set RIGHT and END will set LEFT. + * + * @param gravity The gravity to convert to absolute (horizontal) values. + * @param isRtl Whether the layout is right-to-left. + * @return gravity converted to absolute (horizontal) values. + */ + public static int getAbsoluteGravity(int gravity, boolean isRtl) { + int result = gravity; + // If layout is script specific and gravity is horizontal relative (START or END) + if ((result & RELATIVE_LAYOUT_DIRECTION) > 0) { + if ((result & Gravity.START) == Gravity.START) { + // Remove the START bit + result &= ~START; + if (isRtl) { + // Set the RIGHT bit + result |= RIGHT; + } else { + // Set the LEFT bit + result |= LEFT; + } + } else if ((result & Gravity.END) == Gravity.END) { + // Remove the END bit + result &= ~END; + if (isRtl) { + // Set the LEFT bit + result |= LEFT; + } else { + // Set the RIGHT bit + result |= RIGHT; + } + } + // Don't need the script specific bit any more, so remove it as we are converting to + // absolute values (LEFT or RIGHT) + result &= ~RELATIVE_LAYOUT_DIRECTION; + } + return result; } } diff --git a/core/java/android/view/HardwareCanvas.java b/core/java/android/view/HardwareCanvas.java index caa7b74..23b3abc 100644 --- a/core/java/android/view/HardwareCanvas.java +++ b/core/java/android/view/HardwareCanvas.java @@ -64,6 +64,14 @@ public abstract class HardwareCanvas extends Canvas { abstract boolean drawDisplayList(DisplayList displayList, int width, int height, Rect dirty); /** + * Outputs the specified display list to the log. This method exists for use by + * tools to output display lists for selected nodes to the log. + * + * @param displayList The display list to be logged. + */ + abstract void outputDisplayList(DisplayList displayList); + + /** * Draws the specified layer onto this canvas. * * @param layer The layer to composite on this canvas diff --git a/core/java/android/view/HardwareLayer.java b/core/java/android/view/HardwareLayer.java index d01b8ce..86dec3f 100644 --- a/core/java/android/view/HardwareLayer.java +++ b/core/java/android/view/HardwareLayer.java @@ -26,12 +26,24 @@ import android.graphics.Canvas; * drawn several times. */ abstract class HardwareLayer { + /** + * Indicates an unknown dimension (width or height.) + */ + static final int DIMENSION_UNDEFINED = -1; + int mWidth; int mHeight; final boolean mOpaque; /** + * Creates a new hardware layer with undefined dimensions. + */ + HardwareLayer() { + this(DIMENSION_UNDEFINED, DIMENSION_UNDEFINED, false); + } + + /** * Creates a new hardware layer at least as large as the supplied * dimensions. * diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index 8584bf2..2611ec0 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -17,10 +17,10 @@ package android.view; -import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; -import android.os.SystemClock; +import android.graphics.SurfaceTexture; +import android.os.*; import android.util.EventLog; import android.util.Log; @@ -33,7 +33,7 @@ import javax.microedition.khronos.egl.EGLSurface; import javax.microedition.khronos.opengles.GL; /** - * Interface for rendering a ViewRoot using hardware acceleration. + * Interface for rendering a ViewAncestor using hardware acceleration. * * @hide */ @@ -136,14 +136,14 @@ public abstract class HardwareRenderer { * * @param canvas The Canvas used to render the view. */ - void onHardwarePreDraw(Canvas canvas); + void onHardwarePreDraw(HardwareCanvas canvas); /** * Invoked after a view is drawn by a hardware renderer. * * @param canvas The Canvas used to render the view. */ - void onHardwarePostDraw(Canvas canvas); + void onHardwarePostDraw(HardwareCanvas canvas); } /** @@ -166,6 +166,14 @@ public abstract class HardwareRenderer { abstract DisplayList createDisplayList(View v); /** + * Creates a new hardware layer. A hardware layer built by calling this + * method will be treated as a texture layer, instead of as a render target. + * + * @return A hardware layer + */ + abstract HardwareLayer createHardwareLayer(); + + /** * Creates a new hardware layer. * * @param width The minimum width of the layer @@ -175,10 +183,32 @@ public abstract class HardwareRenderer { * @return A hardware layer */ abstract HardwareLayer createHardwareLayer(int width, int height, boolean isOpaque); + + /** + * Creates a new {@link SurfaceTexture} that can be used to render into the + * specified hardware layer. + * + * + * @param layer The layer to render into using a {@link android.graphics.SurfaceTexture} + * + * @return A {@link SurfaceTexture} + */ + abstract SurfaceTexture createSuraceTexture(HardwareLayer layer); + + /** + * Updates the specified layer. + * + * @param layer The hardware layer to update + * @param width The layer's width + * @param height The layer's height + * @param surface The surface to update + */ + abstract void updateTextureLayer(HardwareLayer layer, int width, int height, + SurfaceTexture surface); /** * Initializes the hardware renderer for the specified surface and setup the - * renderer for drawing, if needed. This is invoked when the ViewRoot has + * renderer for drawing, if needed. This is invoked when the ViewAncestor has * potentially lost the hardware renderer. The hardware renderer should be * reinitialized and setup when the render {@link #isRequested()} and * {@link #isEnabled()}. @@ -256,9 +286,11 @@ public abstract class HardwareRenderer { @SuppressWarnings({"deprecation"}) static abstract class GlRenderer extends HardwareRenderer { - private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; - private static final int EGL_SURFACE_TYPE = 0x3033; - private static final int EGL_SWAP_BEHAVIOR_PRESERVED_BIT = 0x0400; + // These values are not exposed in our EGL APIs + static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + static final int EGL_SURFACE_TYPE = 0x3033; + static final int EGL_SWAP_BEHAVIOR_PRESERVED_BIT = 0x0400; + static final int EGL_OPENGL_ES2_BIT = 4; private static final int SURFACE_STATE_ERROR = 0; private static final int SURFACE_STATE_SUCCESS = 1; @@ -279,6 +311,7 @@ public abstract class HardwareRenderer { Paint mDebugPaint; boolean mDirtyRegions; + final boolean mDirtyRegionsRequested; final int mGlVersion; final boolean mTranslucent; @@ -290,9 +323,10 @@ 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); + mDirtyRegionsRequested = mDirtyRegions; } /** @@ -426,13 +460,12 @@ public abstract class HardwareRenderer { getEGLErrorString(sEgl.eglGetError())); } - sEglConfig = getConfigChooser(mGlVersion).chooseConfig(sEgl, sEglDisplay); + sEglConfig = chooseEglConfig(); if (sEglConfig == null) { // We tried to use EGL_SWAP_BEHAVIOR_PRESERVED_BIT, try again without if (mDirtyRegions) { mDirtyRegions = false; - - sEglConfig = getConfigChooser(mGlVersion).chooseConfig(sEgl, sEglDisplay); + sEglConfig = chooseEglConfig(); if (sEglConfig == null) { throw new RuntimeException("eglConfig not initialized"); } @@ -448,6 +481,21 @@ public abstract class HardwareRenderer { sEglContext = createContext(sEgl, sEglDisplay, sEglConfig); } + private EGLConfig chooseEglConfig() { + int[] configsCount = new int[1]; + EGLConfig[] configs = new EGLConfig[1]; + int[] configSpec = getConfig(mDirtyRegions); + if (!sEgl.eglChooseConfig(sEglDisplay, configSpec, configs, 1, configsCount)) { + throw new IllegalArgumentException("eglChooseConfig failed " + + getEGLErrorString(sEgl.eglGetError())); + } else if (configsCount[0] > 0) { + return configs[0]; + } + return null; + } + + abstract int[] getConfig(boolean dirtyRegions); + GL createEglSurface(SurfaceHolder holder) throws Surface.OutOfResourcesException { // Check preconditions. if (sEgl == null) { @@ -499,11 +547,21 @@ public abstract class HardwareRenderer { throw new Surface.OutOfResourcesException("eglMakeCurrent failed " + getEGLErrorString(sEgl.eglGetError())); } - + + // If mDirtyRegions is set, this means we have an EGL configuration + // with EGL_SWAP_BEHAVIOR_PRESERVED_BIT set if (mDirtyRegions) { if (!GLES20Canvas.preserveBackBuffer()) { Log.w(LOG_TAG, "Backbuffer cannot be preserved"); } + } else if (mDirtyRegionsRequested) { + // If mDirtyRegions is not set, our EGL configuration does not + // have EGL_SWAP_BEHAVIOR_PRESERVED_BIT; however, the default + // swap behavior might be EGL_BUFFER_PRESERVED, which means we + // want to set mDirtyRegions. We try to do this only if dirty + // regions were initially requested as part of the device + // configuration (see RENDER_DIRTY_REGIONS) + mDirtyRegions = GLES20Canvas.isBackBufferPreserved(); } return sEglContext.getGL(); @@ -559,15 +617,6 @@ public abstract class HardwareRenderer { void onPostDraw() { } - - /** - * Defines the EGL configuration for this renderer. - * - * @return An {@link android.view.HardwareRenderer.GlRenderer.EglConfigChooser}. - */ - EglConfigChooser getConfigChooser(int glVersion) { - return new ComponentSizeChooser(glVersion, 8, 8, 8, 8, 0, 0, mDirtyRegions); - } @Override void draw(View view, View.AttachInfo attachInfo, HardwareDrawCallbacks callbacks, @@ -644,8 +693,21 @@ public abstract class HardwareRenderer { } attachInfo.mIgnoreDirtyState = false; - + + final long swapBuffersStartTime; + if (ViewDebug.DEBUG_LATENCY) { + swapBuffersStartTime = System.nanoTime(); + } + sEgl.eglSwapBuffers(sEglDisplay, mEglSurface); + + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(LOG_TAG, "Latency: Spent " + + ((now - swapBuffersStartTime) * 0.000001f) + + "ms waiting for eglSwapBuffers()"); + } + checkEglErrors(); } } @@ -667,134 +729,6 @@ public abstract class HardwareRenderer { } return SURFACE_STATE_SUCCESS; } - - static abstract class EglConfigChooser { - final int[] mConfigSpec; - private final int mGlVersion; - - EglConfigChooser(int glVersion, int[] configSpec) { - mGlVersion = glVersion; - mConfigSpec = filterConfigSpec(configSpec); - } - - EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { - int[] index = new int[1]; - if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, index)) { - throw new IllegalArgumentException("eglChooseConfig failed " - + getEGLErrorString(egl.eglGetError())); - } - - int numConfigs = index[0]; - if (numConfigs <= 0) { - throw new IllegalArgumentException("No configs match configSpec"); - } - - EGLConfig[] configs = new EGLConfig[numConfigs]; - if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, index)) { - throw new IllegalArgumentException("eglChooseConfig failed " - + getEGLErrorString(egl.eglGetError())); - } - - EGLConfig config = chooseConfig(egl, display, configs); - if (config == null) { - throw new IllegalArgumentException("No config chosen"); - } - - return config; - } - - abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs); - - private int[] filterConfigSpec(int[] configSpec) { - if (mGlVersion != 2) { - return configSpec; - } - /* We know none of the subclasses define EGL_RENDERABLE_TYPE. - * And we know the configSpec is well formed. - */ - int len = configSpec.length; - int[] newConfigSpec = new int[len + 2]; - System.arraycopy(configSpec, 0, newConfigSpec, 0, len - 1); - newConfigSpec[len - 1] = EGL10.EGL_RENDERABLE_TYPE; - newConfigSpec[len] = 4; /* EGL_OPENGL_ES2_BIT */ - newConfigSpec[len + 1] = EGL10.EGL_NONE; - return newConfigSpec; - } - } - - /** - * Choose a configuration with exactly the specified r,g,b,a sizes, - * and at least the specified depth and stencil sizes. - */ - static class ComponentSizeChooser extends EglConfigChooser { - private int[] mValue; - - private final int mRedSize; - private final int mGreenSize; - private final int mBlueSize; - private final int mAlphaSize; - private final int mDepthSize; - private final int mStencilSize; - private final boolean mDirtyRegions; - - ComponentSizeChooser(int glVersion, int redSize, int greenSize, int blueSize, - int alphaSize, int depthSize, int stencilSize, boolean dirtyRegions) { - super(glVersion, new int[] { - EGL10.EGL_RED_SIZE, redSize, - EGL10.EGL_GREEN_SIZE, greenSize, - EGL10.EGL_BLUE_SIZE, blueSize, - EGL10.EGL_ALPHA_SIZE, alphaSize, - EGL10.EGL_DEPTH_SIZE, depthSize, - EGL10.EGL_STENCIL_SIZE, stencilSize, - EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT | - (dirtyRegions ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0), - EGL10.EGL_NONE }); - mValue = new int[1]; - mRedSize = redSize; - mGreenSize = greenSize; - mBlueSize = blueSize; - mAlphaSize = alphaSize; - mDepthSize = depthSize; - mStencilSize = stencilSize; - mDirtyRegions = dirtyRegions; - } - - @Override - EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { - for (EGLConfig config : configs) { - int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); - int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); - if (d >= mDepthSize && s >= mStencilSize) { - int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); - int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); - int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); - int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); - boolean backBuffer; - if (mDirtyRegions) { - int surfaceType = findConfigAttrib(egl, display, config, - EGL_SURFACE_TYPE, 0); - backBuffer = (surfaceType & EGL_SWAP_BEHAVIOR_PRESERVED_BIT) != 0; - } else { - backBuffer = true; - } - if (r >= mRedSize && g >= mGreenSize && b >= mBlueSize && a >= mAlphaSize - && backBuffer) { - return config; - } - } - } - return null; - } - - private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, - int attribute, int defaultValue) { - if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { - return mValue[0]; - } - - return defaultValue; - } - } } /** @@ -811,7 +745,23 @@ public abstract class HardwareRenderer { GLES20Canvas createCanvas() { return mGlCanvas = new GLES20Canvas(mTranslucent); } - + + @Override + int[] getConfig(boolean dirtyRegions) { + return new int[] { + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_RED_SIZE, 8, + EGL10.EGL_GREEN_SIZE, 8, + EGL10.EGL_BLUE_SIZE, 8, + EGL10.EGL_ALPHA_SIZE, 8, + EGL10.EGL_DEPTH_SIZE, 0, + EGL10.EGL_STENCIL_SIZE, 0, + EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT | + (dirtyRegions ? EGL_SWAP_BEHAVIOR_PRESERVED_BIT : 0), + EGL10.EGL_NONE + }; + } + @Override boolean canDraw() { return super.canDraw() && mGlCanvas != null; @@ -842,10 +792,26 @@ public abstract class HardwareRenderer { DisplayList createDisplayList(View v) { return new GLES20DisplayList(v); } - + + @Override + HardwareLayer createHardwareLayer() { + return new GLES20TextureLayer(); + } + @Override HardwareLayer createHardwareLayer(int width, int height, boolean isOpaque) { - return new GLES20Layer(width, height, isOpaque); + return new GLES20RenderLayer(width, height, isOpaque); + } + + @Override + SurfaceTexture createSuraceTexture(HardwareLayer layer) { + return ((GLES20TextureLayer) layer).getSurfaceTexture(); + } + + @Override + void updateTextureLayer(HardwareLayer layer, int width, int height, + SurfaceTexture surface) { + ((GLES20TextureLayer) layer).update(width, height, surface.mSurfaceTexture); } static HardwareRenderer create(boolean translucent) { diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 8cb68f9..bfc7c31 100755 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -153,7 +153,14 @@ public final class InputDevice implements Parcelable { * @see #SOURCE_CLASS_POINTER */ public static final int SOURCE_MOUSE = 0x00002000 | SOURCE_CLASS_POINTER; - + + /** + * The input source is a stylus pointing device. + * + * @see #SOURCE_CLASS_POINTER + */ + public static final int SOURCE_STYLUS = 0x00004000 | SOURCE_CLASS_POINTER; + /** * The input source is a trackball. * @@ -585,6 +592,7 @@ public final class InputDevice implements Parcelable { appendSourceDescriptionIfApplicable(description, SOURCE_DPAD, "dpad"); appendSourceDescriptionIfApplicable(description, SOURCE_TOUCHSCREEN, "touchscreen"); appendSourceDescriptionIfApplicable(description, SOURCE_MOUSE, "mouse"); + appendSourceDescriptionIfApplicable(description, SOURCE_STYLUS, "stylus"); appendSourceDescriptionIfApplicable(description, SOURCE_TRACKBALL, "trackball"); appendSourceDescriptionIfApplicable(description, SOURCE_TOUCHPAD, "touchpad"); appendSourceDescriptionIfApplicable(description, SOURCE_JOYSTICK, "joystick"); diff --git a/core/java/android/view/InputEvent.java b/core/java/android/view/InputEvent.java index f6aeb39..01ddcc9 100755 --- a/core/java/android/view/InputEvent.java +++ b/core/java/android/view/InputEvent.java @@ -67,6 +67,52 @@ public abstract class InputEvent implements Parcelable { */ public abstract void setSource(int source); + /** + * Copies the event. + * + * @return A deep copy of the event. + * @hide + */ + public abstract InputEvent copy(); + + /** + * Recycles the event. + * This method should only be used by the system since applications do not + * expect {@link KeyEvent} objects to be recycled, although {@link MotionEvent} + * objects are fine. See {@link KeyEvent#recycle()} for details. + * @hide + */ + public abstract void recycle(); + + /** + * Gets a private flag that indicates when the system has detected that this input event + * may be inconsistent with respect to the sequence of previously delivered input events, + * such as when a key up event is sent but the key was not down or when a pointer + * move event is sent but the pointer is not down. + * + * @return True if this event is tainted. + * @hide + */ + public abstract boolean isTainted(); + + /** + * Sets a private flag that indicates when the system has detected that this input event + * may be inconsistent with respect to the sequence of previously delivered input events, + * such as when a key up event is sent but the key was not down or when a pointer + * move event is sent but the pointer is not down. + * + * @param tainted True if this event is tainted. + * @hide + */ + public abstract void setTainted(boolean tainted); + + /** + * Returns the time (in ns) when this specific event was generated. + * The value is in nanosecond precision but it may not have nanosecond accuracy. + * @hide + */ + public abstract long getEventTimeNano(); + public int describeContents() { return 0; } diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java new file mode 100644 index 0000000..e14b975 --- /dev/null +++ b/core/java/android/view/InputEventConsistencyVerifier.java @@ -0,0 +1,730 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.os.Build; +import android.util.Log; + +/** + * Checks whether a sequence of input events is self-consistent. + * Logs a description of each problem detected. + * <p> + * When a problem is detected, the event is tainted. This mechanism prevents the same + * error from being reported multiple times. + * </p> + * + * @hide + */ +public final class InputEventConsistencyVerifier { + private static final boolean IS_ENG_BUILD = "eng".equals(Build.TYPE); + + // The number of recent events to log when a problem is detected. + // Can be set to 0 to disable logging recent events but the runtime overhead of + // this feature is negligible on current hardware. + private static final int RECENT_EVENTS_TO_LOG = 5; + + // The object to which the verifier is attached. + private final Object mCaller; + + // Consistency verifier flags. + private final int mFlags; + + // Tag for logging which a client can set to help distinguish the output + // from different verifiers since several can be active at the same time. + // If not provided defaults to the simple class name. + private final String mLogTag; + + // The most recently checked event and the nesting level at which it was checked. + // This is only set when the verifier is called from a nesting level greater than 0 + // so that the verifier can detect when it has been asked to verify the same event twice. + // It does not make sense to examine the contents of the last event since it may have + // been recycled. + private InputEvent mLastEvent; + private int mLastNestingLevel; + + // Copy of the most recent events. + private InputEvent[] mRecentEvents; + private boolean[] mRecentEventsUnhandled; + private int mMostRecentEventIndex; + + // Current event and its type. + private InputEvent mCurrentEvent; + private String mCurrentEventType; + + // Linked list of key state objects. + private KeyState mKeyStateList; + + // Current state of the trackball. + private boolean mTrackballDown; + private boolean mTrackballUnhandled; + + // Bitfield of pointer ids that are currently down. + // Assumes that the largest possible pointer id is 31, which is potentially subject to change. + // (See MAX_POINTER_ID in frameworks/base/include/ui/Input.h) + private int mTouchEventStreamPointers; + + // The device id and source of the current stream of touch events. + private int mTouchEventStreamDeviceId = -1; + private int mTouchEventStreamSource; + + // Set to true when we discover that the touch event stream is inconsistent. + // Reset on down or cancel. + private boolean mTouchEventStreamIsTainted; + + // Set to true if the touch event stream is partially unhandled. + private boolean mTouchEventStreamUnhandled; + + // Set to true if we received hover enter. + private boolean mHoverEntered; + + // The current violation message. + private StringBuilder mViolationMessage; + + /** + * Indicates that the verifier is intended to act on raw device input event streams. + * Disables certain checks for invariants that are established by the input dispatcher + * itself as it delivers input events, such as key repeating behavior. + */ + public static final int FLAG_RAW_DEVICE_INPUT = 1 << 0; + + /** + * Creates an input consistency verifier. + * @param caller The object to which the verifier is attached. + * @param flags Flags to the verifier, or 0 if none. + */ + public InputEventConsistencyVerifier(Object caller, int flags) { + this(caller, flags, InputEventConsistencyVerifier.class.getSimpleName()); + } + + /** + * Creates an input consistency verifier. + * @param caller The object to which the verifier is attached. + * @param flags Flags to the verifier, or 0 if none. + * @param logTag Tag for logging. If null defaults to the short class name. + */ + public InputEventConsistencyVerifier(Object caller, int flags, String logTag) { + this.mCaller = caller; + this.mFlags = flags; + this.mLogTag = (logTag != null) ? logTag : "InputEventConsistencyVerifier"; + } + + /** + * Determines whether the instrumentation should be enabled. + * @return True if it should be enabled. + */ + public static boolean isInstrumentationEnabled() { + return IS_ENG_BUILD; + } + + /** + * Resets the state of the input event consistency verifier. + */ + public void reset() { + mLastEvent = null; + mLastNestingLevel = 0; + mTrackballDown = false; + mTrackballUnhandled = false; + mTouchEventStreamPointers = 0; + mTouchEventStreamIsTainted = false; + mTouchEventStreamUnhandled = false; + mHoverEntered = false; + + while (mKeyStateList != null) { + final KeyState state = mKeyStateList; + mKeyStateList = state.next; + state.recycle(); + } + } + + /** + * Checks an arbitrary input event. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onInputEvent(InputEvent event, int nestingLevel) { + if (event instanceof KeyEvent) { + final KeyEvent keyEvent = (KeyEvent)event; + onKeyEvent(keyEvent, nestingLevel); + } else { + final MotionEvent motionEvent = (MotionEvent)event; + if (motionEvent.isTouchEvent()) { + onTouchEvent(motionEvent, nestingLevel); + } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + onTrackballEvent(motionEvent, nestingLevel); + } else { + onGenericMotionEvent(motionEvent, nestingLevel); + } + } + } + + /** + * Checks a key event. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onKeyEvent(KeyEvent event, int nestingLevel) { + if (!startEvent(event, nestingLevel, "KeyEvent")) { + return; + } + + try { + ensureMetaStateIsNormalized(event.getMetaState()); + + final int action = event.getAction(); + final int deviceId = event.getDeviceId(); + final int source = event.getSource(); + final int keyCode = event.getKeyCode(); + switch (action) { + case KeyEvent.ACTION_DOWN: { + KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ false); + if (state != null) { + // If the key is already down, ensure it is a repeat. + // We don't perform this check when processing raw device input + // because the input dispatcher itself is responsible for setting + // the key repeat count before it delivers input events. + if (state.unhandled) { + state.unhandled = false; + } else if ((mFlags & FLAG_RAW_DEVICE_INPUT) == 0 + && event.getRepeatCount() == 0) { + problem("ACTION_DOWN but key is already down and this event " + + "is not a key repeat."); + } + } else { + addKeyState(deviceId, source, keyCode); + } + break; + } + case KeyEvent.ACTION_UP: { + KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ true); + if (state == null) { + problem("ACTION_UP but key was not down."); + } else { + state.recycle(); + } + break; + } + case KeyEvent.ACTION_MULTIPLE: + break; + default: + problem("Invalid action " + KeyEvent.actionToString(action) + + " for key event."); + break; + } + } finally { + finishEvent(false); + } + } + + /** + * Checks a trackball event. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onTrackballEvent(MotionEvent event, int nestingLevel) { + if (!startEvent(event, nestingLevel, "TrackballEvent")) { + return; + } + + try { + ensureMetaStateIsNormalized(event.getMetaState()); + + final int action = event.getAction(); + final int source = event.getSource(); + if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + switch (action) { + case MotionEvent.ACTION_DOWN: + if (mTrackballDown && !mTrackballUnhandled) { + problem("ACTION_DOWN but trackball is already down."); + } else { + mTrackballDown = true; + mTrackballUnhandled = false; + } + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + break; + case MotionEvent.ACTION_UP: + if (!mTrackballDown) { + problem("ACTION_UP but trackball is not down."); + } else { + mTrackballDown = false; + mTrackballUnhandled = false; + } + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + break; + case MotionEvent.ACTION_MOVE: + ensurePointerCountIsOneForThisAction(event); + break; + default: + problem("Invalid action " + MotionEvent.actionToString(action) + + " for trackball event."); + break; + } + + if (mTrackballDown && event.getPressure() <= 0) { + problem("Trackball is down but pressure is not greater than 0."); + } else if (!mTrackballDown && event.getPressure() != 0) { + problem("Trackball is up but pressure is not equal to 0."); + } + } else { + problem("Source was not SOURCE_CLASS_TRACKBALL."); + } + } finally { + finishEvent(false); + } + } + + /** + * Checks a touch event. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onTouchEvent(MotionEvent event, int nestingLevel) { + if (!startEvent(event, nestingLevel, "TouchEvent")) { + return; + } + + final int action = event.getAction(); + final boolean newStream = action == MotionEvent.ACTION_DOWN + || action == MotionEvent.ACTION_CANCEL; + if (mTouchEventStreamIsTainted || mTouchEventStreamUnhandled) { + if (newStream) { + mTouchEventStreamIsTainted = false; + mTouchEventStreamUnhandled = false; + mTouchEventStreamPointers = 0; + } else { + finishEvent(mTouchEventStreamIsTainted); + return; + } + } + + try { + ensureMetaStateIsNormalized(event.getMetaState()); + + final int deviceId = event.getDeviceId(); + final int source = event.getSource(); + + if (!newStream && mTouchEventStreamDeviceId != -1 + && (mTouchEventStreamDeviceId != deviceId + || mTouchEventStreamSource != source)) { + problem("Touch event stream contains events from multiple sources: " + + "previous device id " + mTouchEventStreamDeviceId + + ", previous source " + Integer.toHexString(mTouchEventStreamSource) + + ", new device id " + deviceId + + ", new source " + Integer.toHexString(source)); + } + mTouchEventStreamDeviceId = deviceId; + mTouchEventStreamSource = source; + + final int pointerCount = event.getPointerCount(); + if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (action) { + case MotionEvent.ACTION_DOWN: + if (mTouchEventStreamPointers != 0) { + problem("ACTION_DOWN but pointers are already down. " + + "Probably missing ACTION_UP from previous gesture."); + } + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + mTouchEventStreamPointers = 1 << event.getPointerId(0); + break; + case MotionEvent.ACTION_UP: + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + mTouchEventStreamPointers = 0; + mTouchEventStreamIsTainted = false; + break; + case MotionEvent.ACTION_MOVE: { + final int expectedPointerCount = + Integer.bitCount(mTouchEventStreamPointers); + if (pointerCount != expectedPointerCount) { + problem("ACTION_MOVE contained " + pointerCount + + " pointers but there are currently " + + expectedPointerCount + " pointers down."); + mTouchEventStreamIsTainted = true; + } + break; + } + case MotionEvent.ACTION_CANCEL: + mTouchEventStreamPointers = 0; + mTouchEventStreamIsTainted = false; + break; + case MotionEvent.ACTION_OUTSIDE: + if (mTouchEventStreamPointers != 0) { + problem("ACTION_OUTSIDE but pointers are still down."); + } + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + mTouchEventStreamIsTainted = false; + break; + default: { + final int actionMasked = event.getActionMasked(); + final int actionIndex = event.getActionIndex(); + if (actionMasked == MotionEvent.ACTION_POINTER_DOWN) { + if (mTouchEventStreamPointers == 0) { + problem("ACTION_POINTER_DOWN but no other pointers were down."); + mTouchEventStreamIsTainted = true; + } + if (actionIndex < 0 || actionIndex >= pointerCount) { + problem("ACTION_POINTER_DOWN index is " + actionIndex + + " but the pointer count is " + pointerCount + "."); + mTouchEventStreamIsTainted = true; + } else { + final int id = event.getPointerId(actionIndex); + final int idBit = 1 << id; + if ((mTouchEventStreamPointers & idBit) != 0) { + problem("ACTION_POINTER_DOWN specified pointer id " + id + + " which is already down."); + mTouchEventStreamIsTainted = true; + } else { + mTouchEventStreamPointers |= idBit; + } + } + ensureHistorySizeIsZeroForThisAction(event); + } else if (actionMasked == MotionEvent.ACTION_POINTER_UP) { + if (actionIndex < 0 || actionIndex >= pointerCount) { + problem("ACTION_POINTER_UP index is " + actionIndex + + " but the pointer count is " + pointerCount + "."); + mTouchEventStreamIsTainted = true; + } else { + final int id = event.getPointerId(actionIndex); + final int idBit = 1 << id; + if ((mTouchEventStreamPointers & idBit) == 0) { + problem("ACTION_POINTER_UP specified pointer id " + id + + " which is not currently down."); + mTouchEventStreamIsTainted = true; + } else { + mTouchEventStreamPointers &= ~idBit; + } + } + ensureHistorySizeIsZeroForThisAction(event); + } else { + problem("Invalid action " + MotionEvent.actionToString(action) + + " for touch event."); + } + break; + } + } + } else { + problem("Source was not SOURCE_CLASS_POINTER."); + } + } finally { + finishEvent(false); + } + } + + /** + * Checks a generic motion event. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onGenericMotionEvent(MotionEvent event, int nestingLevel) { + if (!startEvent(event, nestingLevel, "GenericMotionEvent")) { + return; + } + + try { + ensureMetaStateIsNormalized(event.getMetaState()); + + final int action = event.getAction(); + final int source = event.getSource(); + if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (action) { + case MotionEvent.ACTION_HOVER_ENTER: + ensurePointerCountIsOneForThisAction(event); + mHoverEntered = true; + break; + case MotionEvent.ACTION_HOVER_MOVE: + ensurePointerCountIsOneForThisAction(event); + break; + case MotionEvent.ACTION_HOVER_EXIT: + ensurePointerCountIsOneForThisAction(event); + if (!mHoverEntered) { + problem("ACTION_HOVER_EXIT without prior ACTION_HOVER_ENTER"); + } + mHoverEntered = false; + break; + case MotionEvent.ACTION_SCROLL: + ensureHistorySizeIsZeroForThisAction(event); + ensurePointerCountIsOneForThisAction(event); + break; + default: + problem("Invalid action for generic pointer event."); + break; + } + } else if ((source & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { + switch (action) { + case MotionEvent.ACTION_MOVE: + ensurePointerCountIsOneForThisAction(event); + break; + default: + problem("Invalid action for generic joystick event."); + break; + } + } + } finally { + finishEvent(false); + } + } + + /** + * Notifies the verifier that a given event was unhandled and the rest of the + * trace for the event should be ignored. + * This method should only be called if the event was previously checked by + * the consistency verifier using {@link #onInputEvent} and other methods. + * @param event The event. + * @param nestingLevel The nesting level: 0 if called from the base class, + * or 1 from a subclass. If the event was already checked by this consistency verifier + * at a higher nesting level, it will not be checked again. Used to handle the situation + * where a subclass dispatching method delegates to its superclass's dispatching method + * and both dispatching methods call into the consistency verifier. + */ + public void onUnhandledEvent(InputEvent event, int nestingLevel) { + if (nestingLevel != mLastNestingLevel) { + return; + } + + if (mRecentEventsUnhandled != null) { + mRecentEventsUnhandled[mMostRecentEventIndex] = true; + } + + if (event instanceof KeyEvent) { + final KeyEvent keyEvent = (KeyEvent)event; + final int deviceId = keyEvent.getDeviceId(); + final int source = keyEvent.getSource(); + final int keyCode = keyEvent.getKeyCode(); + final KeyState state = findKeyState(deviceId, source, keyCode, /*remove*/ false); + if (state != null) { + state.unhandled = true; + } + } else { + final MotionEvent motionEvent = (MotionEvent)event; + if (motionEvent.isTouchEvent()) { + mTouchEventStreamUnhandled = true; + } else if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + if (mTrackballDown) { + mTrackballUnhandled = true; + } + } + } + } + + private void ensureMetaStateIsNormalized(int metaState) { + final int normalizedMetaState = KeyEvent.normalizeMetaState(metaState); + if (normalizedMetaState != metaState) { + problem(String.format("Metastate not normalized. Was 0x%08x but expected 0x%08x.", + metaState, normalizedMetaState)); + } + } + + private void ensurePointerCountIsOneForThisAction(MotionEvent event) { + final int pointerCount = event.getPointerCount(); + if (pointerCount != 1) { + problem("Pointer count is " + pointerCount + " but it should always be 1 for " + + MotionEvent.actionToString(event.getAction())); + } + } + + private void ensureHistorySizeIsZeroForThisAction(MotionEvent event) { + final int historySize = event.getHistorySize(); + if (historySize != 0) { + problem("History size is " + historySize + " but it should always be 0 for " + + MotionEvent.actionToString(event.getAction())); + } + } + + private boolean startEvent(InputEvent event, int nestingLevel, String eventType) { + // Ignore the event if it is already tainted. + if (event.isTainted()) { + return false; + } + + // Ignore the event if we already checked it at a higher nesting level. + if (event == mLastEvent && nestingLevel < mLastNestingLevel) { + return false; + } + + if (nestingLevel > 0) { + mLastEvent = event; + mLastNestingLevel = nestingLevel; + } else { + mLastEvent = null; + mLastNestingLevel = 0; + } + + mCurrentEvent = event; + mCurrentEventType = eventType; + return true; + } + + private void finishEvent(boolean tainted) { + if (mViolationMessage != null && mViolationMessage.length() != 0) { + mViolationMessage.append("\n in ").append(mCaller); + mViolationMessage.append("\n "); + appendEvent(mViolationMessage, 0, mCurrentEvent, false); + + if (RECENT_EVENTS_TO_LOG != 0 && mRecentEvents != null) { + mViolationMessage.append("\n -- recent events --"); + for (int i = 0; i < RECENT_EVENTS_TO_LOG; i++) { + final int index = (mMostRecentEventIndex + RECENT_EVENTS_TO_LOG - i) + % RECENT_EVENTS_TO_LOG; + final InputEvent event = mRecentEvents[index]; + if (event == null) { + break; + } + mViolationMessage.append("\n "); + appendEvent(mViolationMessage, i + 1, event, mRecentEventsUnhandled[index]); + } + } + + Log.d(mLogTag, mViolationMessage.toString()); + mViolationMessage.setLength(0); + tainted = true; + } + + if (tainted) { + // Taint the event so that we do not generate additional violations from it + // further downstream. + mCurrentEvent.setTainted(true); + } + + if (RECENT_EVENTS_TO_LOG != 0) { + if (mRecentEvents == null) { + mRecentEvents = new InputEvent[RECENT_EVENTS_TO_LOG]; + mRecentEventsUnhandled = new boolean[RECENT_EVENTS_TO_LOG]; + } + final int index = (mMostRecentEventIndex + 1) % RECENT_EVENTS_TO_LOG; + mMostRecentEventIndex = index; + if (mRecentEvents[index] != null) { + mRecentEvents[index].recycle(); + } + mRecentEvents[index] = mCurrentEvent.copy(); + mRecentEventsUnhandled[index] = false; + } + + mCurrentEvent = null; + mCurrentEventType = null; + } + + private static void appendEvent(StringBuilder message, int index, + InputEvent event, boolean unhandled) { + message.append(index).append(": sent at ").append(event.getEventTimeNano()); + message.append(", "); + if (unhandled) { + message.append("(unhandled) "); + } + message.append(event); + } + + private void problem(String message) { + if (mViolationMessage == null) { + mViolationMessage = new StringBuilder(); + } + if (mViolationMessage.length() == 0) { + mViolationMessage.append(mCurrentEventType).append(": "); + } else { + mViolationMessage.append("\n "); + } + mViolationMessage.append(message); + } + + private KeyState findKeyState(int deviceId, int source, int keyCode, boolean remove) { + KeyState last = null; + KeyState state = mKeyStateList; + while (state != null) { + if (state.deviceId == deviceId && state.source == source + && state.keyCode == keyCode) { + if (remove) { + if (last != null) { + last.next = state.next; + } else { + mKeyStateList = state.next; + } + state.next = null; + } + return state; + } + last = state; + state = state.next; + } + return null; + } + + private void addKeyState(int deviceId, int source, int keyCode) { + KeyState state = KeyState.obtain(deviceId, source, keyCode); + state.next = mKeyStateList; + mKeyStateList = state; + } + + private static final class KeyState { + private static Object mRecycledListLock = new Object(); + private static KeyState mRecycledList; + + public KeyState next; + public int deviceId; + public int source; + public int keyCode; + public boolean unhandled; + + private KeyState() { + } + + public static KeyState obtain(int deviceId, int source, int keyCode) { + KeyState state; + synchronized (mRecycledListLock) { + state = mRecycledList; + if (state != null) { + mRecycledList = state.next; + } else { + state = new KeyState(); + } + } + state.deviceId = deviceId; + state.source = source; + state.keyCode = keyCode; + state.unhandled = false; + return state; + } + + public void recycle() { + synchronized (mRecycledListLock) { + next = mRecycledList; + mRecycledList = next; + } + } + } +} diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index f455f91..5dbda90 100755 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -566,6 +566,19 @@ public class KeyEvent extends InputEvent implements Parcelable { public static final int KEYCODE_BUTTON_15 = 202; /** Key code constant: Generic Game Pad Button #16.*/ public static final int KEYCODE_BUTTON_16 = 203; + /** Key code constant: Language Switch key. + * Toggles the current input language such as switching between English and Japanese on + * a QWERTY keyboard. On some devices, the same function may be performed by + * pressing Shift+Spacebar. */ + public static final int KEYCODE_LANGUAGE_SWITCH = 204; + /** Key code constant: Manner Mode key. + * Toggles silent or vibrate mode on and off to make the device behave more politely + * in certain settings such as on a crowded train. On some devices, the key may only + * operate when long-pressed. */ + public static final int KEYCODE_MANNER_MODE = 205; + /** Key code constant: 3D Mode key. + * Toggles the display between 2D and 3D mode. */ + public static final int KEYCODE_3D_MODE = 206; private static final int LAST_KEYCODE = KEYCODE_BUTTON_16; @@ -791,6 +804,9 @@ public class KeyEvent extends InputEvent implements Parcelable { names.append(KEYCODE_BUTTON_14, "KEYCODE_BUTTON_14"); names.append(KEYCODE_BUTTON_15, "KEYCODE_BUTTON_15"); names.append(KEYCODE_BUTTON_16, "KEYCODE_BUTTON_16"); + names.append(KEYCODE_LANGUAGE_SWITCH, "KEYCODE_LANGUAGE_SWITCH"); + names.append(KEYCODE_MANNER_MODE, "KEYCODE_MANNER_MODE"); + names.append(KEYCODE_3D_MODE, "KEYCODE_3D_MODE"); }; // Symbolic names of all metakeys in bit order from least significant to most significant. @@ -1154,7 +1170,18 @@ public class KeyEvent extends InputEvent implements Parcelable { * @hide */ public static final int FLAG_START_TRACKING = 0x40000000; - + + /** + * Private flag that indicates when the system has detected that this key event + * may be inconsistent with respect to the sequence of previously delivered key events, + * such as when a key up event is sent but the key was not down. + * + * @hide + * @see #isTainted + * @see #setTainted + */ + public static final int FLAG_TAINTED = 0x80000000; + /** * Returns the maximum keycode. */ @@ -1519,6 +1546,33 @@ public class KeyEvent extends InputEvent implements Parcelable { } /** + * Obtains a (potentially recycled) copy of another key event. + * + * @hide + */ + public static KeyEvent obtain(KeyEvent other) { + KeyEvent ev = obtain(); + ev.mDownTime = other.mDownTime; + ev.mEventTime = other.mEventTime; + ev.mAction = other.mAction; + ev.mKeyCode = other.mKeyCode; + ev.mRepeatCount = other.mRepeatCount; + ev.mMetaState = other.mMetaState; + ev.mDeviceId = other.mDeviceId; + ev.mScanCode = other.mScanCode; + ev.mFlags = other.mFlags; + ev.mSource = other.mSource; + ev.mCharacters = other.mCharacters; + return ev; + } + + /** @hide */ + @Override + public KeyEvent copy() { + return obtain(this); + } + + /** * Recycles a key event. * Key events should only be recycled if they are owned by the system since user * code expects them to be essentially immutable, "tracking" notwithstanding. @@ -1619,7 +1673,19 @@ public class KeyEvent extends InputEvent implements Parcelable { event.mFlags = flags; return event; } - + + /** @hide */ + @Override + public final boolean isTainted() { + return (mFlags & FLAG_TAINTED) != 0; + } + + /** @hide */ + @Override + public final void setTainted(boolean tainted) { + mFlags = tainted ? mFlags | FLAG_TAINTED : mFlags & ~FLAG_TAINTED; + } + /** * Don't use in new code, instead explicitly check * {@link #getAction()}. @@ -2274,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}. * @@ -2600,15 +2672,22 @@ public class KeyEvent extends InputEvent implements Parcelable { @Override public String toString() { - return "KeyEvent{action=" + actionToString(mAction) - + " keycode=" + keyCodeToString(mKeyCode) - + " scancode=" + mScanCode - + " metaState=" + metaStateToString(mMetaState) - + " flags=0x" + Integer.toHexString(mFlags) - + " repeat=" + mRepeatCount - + " device=" + mDeviceId - + " source=0x" + Integer.toHexString(mSource) - + "}"; + StringBuilder msg = new StringBuilder(); + msg.append("KeyEvent { action=").append(actionToString(mAction)); + msg.append(", keyCode=").append(keyCodeToString(mKeyCode)); + msg.append(", scanCode=").append(mScanCode); + if (mCharacters != null) { + msg.append(", characters=\"").append(mCharacters).append("\""); + } + msg.append(", metaState=").append(metaStateToString(mMetaState)); + msg.append(", flags=0x").append(Integer.toHexString(mFlags)); + msg.append(", repeatCount=").append(mRepeatCount); + msg.append(", eventTime=").append(mEventTime); + msg.append(", downTime=").append(mDownTime); + msg.append(", deviceId=").append(mDeviceId); + msg.append(", source=0x").append(Integer.toHexString(mSource)); + msg.append(" }"); + return msg.toString(); } /** diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java index 81346b4..332a0fa 100644 --- a/core/java/android/view/LayoutInflater.java +++ b/core/java/android/view/LayoutInflater.java @@ -16,6 +16,10 @@ package android.view; +import android.graphics.Canvas; +import android.os.Handler; +import android.os.Message; +import android.widget.FrameLayout; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -83,6 +87,7 @@ public abstract class LayoutInflater { private static final String TAG_MERGE = "merge"; private static final String TAG_INCLUDE = "include"; + private static final String TAG_1995 = "blink"; private static final String TAG_REQUEST_FOCUS = "requestFocus"; /** @@ -454,7 +459,12 @@ public abstract class LayoutInflater { rInflate(parser, root, attrs, false); } else { // Temp is the root view that was found in the xml - View temp = createViewFromTag(root, name, attrs); + View temp; + if (TAG_1995.equals(name)) { + temp = new BlinkLayout(mContext, attrs); + } else { + temp = createViewFromTag(root, name, attrs); + } ViewGroup.LayoutParams params = null; @@ -605,10 +615,9 @@ public abstract class LayoutInflater { * Throw an exception because the specified class is not allowed to be inflated. */ private void failNotAllowed(String name, String prefix, AttributeSet attrs) { - InflateException ie = new InflateException(attrs.getPositionDescription() + throw new InflateException(attrs.getPositionDescription() + ": Class not allowed to be inflated " + (prefix != null ? (prefix + name) : name)); - throw ie; } /** @@ -720,6 +729,12 @@ public abstract class LayoutInflater { parseInclude(parser, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException("<merge /> must be the root element"); + } else if (TAG_1995.equals(name)) { + final View view = new BlinkLayout(mContext, attrs); + final ViewGroup viewGroup = (ViewGroup) parent; + final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); + rInflate(parser, view, attrs, true); + viewGroup.addView(view, params); } else { final View view = createViewFromTag(parent, name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; @@ -847,5 +862,64 @@ public abstract class LayoutInflater { parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) { // Empty } - } + } + + private static class BlinkLayout extends FrameLayout { + private static final int MESSAGE_BLINK = 0x42; + private static final int BLINK_DELAY = 500; + + private boolean mBlink; + private boolean mBlinkState; + private final Handler mHandler; + + public BlinkLayout(Context context, AttributeSet attrs) { + super(context, attrs); + mHandler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(Message msg) { + if (msg.what == MESSAGE_BLINK) { + if (mBlink) { + mBlinkState = !mBlinkState; + makeBlink(); + } + invalidate(); + return true; + } + return false; + } + }); + } + + private void makeBlink() { + Message message = mHandler.obtainMessage(MESSAGE_BLINK); + mHandler.sendMessageDelayed(message, BLINK_DELAY); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + mBlink = true; + mBlinkState = true; + + makeBlink(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + mBlink = false; + mBlinkState = true; + + mHandler.removeMessages(MESSAGE_BLINK); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (mBlinkState) { + super.dispatchDraw(canvas); + } + } + } } diff --git a/core/java/android/view/MenuItem.java b/core/java/android/view/MenuItem.java index 780c52e..dc68264 100644 --- a/core/java/android/view/MenuItem.java +++ b/core/java/android/view/MenuItem.java @@ -51,6 +51,13 @@ public interface MenuItem { * it also has an icon specified. */ public static final int SHOW_AS_ACTION_WITH_TEXT = 4; + + /** + * This item's action view collapses to a normal menu item. + * When expanded, the action view temporarily takes over + * a larger segment of its container. + */ + public static final int SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW = 8; /** * Interface definition for a callback to be invoked when a menu item is @@ -74,6 +81,34 @@ public interface MenuItem { } /** + * Interface definition for a callback to be invoked when a menu item + * marked with {@link MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} is + * expanded or collapsed. + * + * @see MenuItem#expandActionView() + * @see MenuItem#collapseActionView() + * @see MenuItem#setShowAsActionFlags(int) + * @see MenuItem# + */ + public interface OnActionExpandListener { + /** + * Called when a menu item with {@link MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} + * is expanded. + * @param item Item that was expanded + * @return true if the item should expand, false if expansion should be suppressed. + */ + public boolean onMenuItemActionExpand(MenuItem item); + + /** + * Called when a menu item with {@link MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} + * is collapsed. + * @param item Item that was collapsed + * @return true if the item should collapse, false if collapsing should be suppressed. + */ + public boolean onMenuItemActionCollapse(MenuItem item); + } + + /** * Return the identifier for this menu item. The identifier can not * be changed after the menu is created. * @@ -421,6 +456,27 @@ public interface MenuItem { public void setShowAsAction(int actionEnum); /** + * Sets how this item should display in the presence of an Action Bar. + * The parameter actionEnum is a flag set. One of {@link #SHOW_AS_ACTION_ALWAYS}, + * {@link #SHOW_AS_ACTION_IF_ROOM}, or {@link #SHOW_AS_ACTION_NEVER} should + * be used, and you may optionally OR the value with {@link #SHOW_AS_ACTION_WITH_TEXT}. + * SHOW_AS_ACTION_WITH_TEXT requests that when the item is shown as an action, + * it should be shown with a text label. + * + * <p>Note: This method differs from {@link #setShowAsAction(int)} only in that it + * returns the current MenuItem instance for call chaining. + * + * @param actionEnum How the item should display. One of + * {@link #SHOW_AS_ACTION_ALWAYS}, {@link #SHOW_AS_ACTION_IF_ROOM}, or + * {@link #SHOW_AS_ACTION_NEVER}. SHOW_AS_ACTION_NEVER is the default. + * + * @see android.app.ActionBar + * @see #setActionView(View) + * @return This MenuItem instance for call chaining. + */ + public MenuItem setShowAsActionFlags(int actionEnum); + + /** * Set an action view for this menu item. An action view will be displayed in place * of an automatically generated menu item element in the UI when this item is shown * as an action within a parent. @@ -453,4 +509,52 @@ public interface MenuItem { * @see #setShowAsAction(int) */ public View getActionView(); + + /** + * Expand the action view associated with this menu item. + * The menu item must have an action view set, as well as + * the showAsAction flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. + * If a listener has been set using {@link #setOnActionExpandListener(OnActionExpandListener)} + * it will have its {@link OnActionExpandListener#onMenuItemActionExpand(MenuItem)} + * method invoked. The listener may return false from this method to prevent expanding + * the action view. + * + * @return true if the action view was expanded, false otherwise. + */ + public boolean expandActionView(); + + /** + * Collapse the action view associated with this menu item. + * The menu item must have an action view set, as well as the showAsAction flag + * {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. If a listener has been set using + * {@link #setOnActionExpandListener(OnActionExpandListener)} it will have its + * {@link OnActionExpandListener#onMenuItemActionCollapse(MenuItem)} method invoked. + * The listener may return false from this method to prevent collapsing the action view. + * + * @return true if the action view was collapsed, false otherwise. + */ + public boolean collapseActionView(); + + /** + * Returns true if this menu item's action view has been expanded. + * + * @return true if the item's action view is expanded, false otherwise. + * + * @see #expandActionView() + * @see #collapseActionView() + * @see #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW + * @see OnActionExpandListener + */ + public boolean isActionViewExpanded(); + + /** + * Set an {@link OnActionExpandListener} on this menu item to be notified when + * the associated action view is expanded or collapsed. The menu item must + * be configured to expand or collapse its action view using the flag + * {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. + * + * @param listener Listener that will respond to expand/collapse events + * @return This menu item instance for call chaining + */ + public MenuItem setOnActionExpandListener(OnActionExpandListener listener); }
\ No newline at end of file diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index a17db5d..3436cd1 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -23,53 +23,60 @@ import android.os.SystemClock; import android.util.SparseArray; /** - * Object used to report movement (mouse, pen, finger, trackball) events. This - * class may hold either absolute or relative movements, depending on what - * it is being used for. + * Object used to report movement (mouse, pen, finger, trackball) events. + * Motion events may hold either absolute or relative movements and other data, + * depending on the type of device. + * + * <h3>Overview</h3> * <p> - * On pointing devices with source class {@link InputDevice#SOURCE_CLASS_POINTER} - * such as touch screens, the pointer coordinates specify absolute - * positions such as view X/Y coordinates. Each complete gesture is represented - * by a sequence of motion events with actions that describe pointer state transitions - * and movements. A gesture starts with a motion event with {@link #ACTION_DOWN} - * that provides the location of the first pointer down. As each additional - * pointer that goes down or up, the framework will generate a motion event with - * {@link #ACTION_POINTER_DOWN} or {@link #ACTION_POINTER_UP} accordingly. - * Pointer movements are described by motion events with {@link #ACTION_MOVE}. - * Finally, a gesture end either when the final pointer goes up as represented - * by a motion event with {@link #ACTION_UP} or when gesture is canceled - * with {@link #ACTION_CANCEL}. + * Motion events describe movements in terms of an action code and a set of axis values. + * The action code specifies the state change that occurred such as a pointer going + * down or up. The axis values describe the position and other movement properties. * </p><p> - * Some pointing devices such as mice may support vertical and/or horizontal scrolling. - * A scroll event is reported as a generic motion event with {@link #ACTION_SCROLL} that - * includes the relative scroll offset in the {@link #AXIS_VSCROLL} and - * {@link #AXIS_HSCROLL} axes. See {@link #getAxisValue(int)} for information - * about retrieving these additional axes. + * For example, when the user first touches the screen, the system delivers a touch + * event to the appropriate {@link View} with the action code {@link #ACTION_DOWN} + * and a set of axis values that include the X and Y coordinates of the touch and + * information about the pressure, size and orientation of the contact area. * </p><p> - * On trackball devices with source class {@link InputDevice#SOURCE_CLASS_TRACKBALL}, - * the pointer coordinates specify relative movements as X/Y deltas. - * A trackball gesture consists of a sequence of movements described by motion - * events with {@link #ACTION_MOVE} interspersed with occasional {@link #ACTION_DOWN} - * or {@link #ACTION_UP} motion events when the trackball button is pressed or released. + * Some devices can report multiple movement traces at the same time. Multi-touch + * screens emit one movement trace for each finger. The individual fingers or + * other objects that generate movement traces are referred to as <em>pointers</em>. + * Motion events contain information about all of the pointers that are currently active + * even if some of them have not moved since the last event was delivered. * </p><p> - * On joystick devices with source class {@link InputDevice#SOURCE_CLASS_JOYSTICK}, - * the pointer coordinates specify the absolute position of the joystick axes. - * The joystick axis values are normalized to a range of -1.0 to 1.0 where 0.0 corresponds - * to the center position. More information about the set of available axes and the - * range of motion can be obtained using {@link InputDevice#getMotionRange}. - * Some common joystick axes are {@link #AXIS_X}, {@link #AXIS_Y}, - * {@link #AXIS_HAT_X}, {@link #AXIS_HAT_Y}, {@link #AXIS_Z} and {@link #AXIS_RZ}. - * </p><p> - * Motion events always report movements for all pointers at once. The number - * of pointers only ever changes by one as individual pointers go up and down, + * The number of pointers only ever changes by one as individual pointers go up and down, * except when the gesture is canceled. * </p><p> - * The order in which individual pointers appear within a motion event can change - * from one event to the next. Use the {@link #getPointerId(int)} method to obtain a - * pointer id to track pointers across motion events in a gesture. Then for - * successive motion events, use the {@link #findPointerIndex(int)} method to obtain - * the pointer index for a given pointer id in that motion event. + * Each pointer has a unique id that is assigned when it first goes down + * (indicated by {@link #ACTION_DOWN} or {@link #ACTION_POINTER_DOWN}). A pointer id + * remains valid until the pointer eventually goes up (indicated by {@link #ACTION_UP} + * or {@link #ACTION_POINTER_UP}) or when the gesture is canceled (indicated by + * {@link #ACTION_CANCEL}). * </p><p> + * The MotionEvent class provides many methods to query the position and other properties of + * pointers, such as {@link #getX(int)}, {@link #getY(int)}, {@link #getAxisValue}, + * {@link #getPointerId(int)}, {@link #getToolType(int)}, and many others. Most of these + * methods accept the pointer index as a parameter rather than the pointer id. + * The pointer index of each pointer in the event ranges from 0 to one less than the value + * returned by {@link #getPointerCount()}. + * </p><p> + * The order in which individual pointers appear within a motion event is undefined. + * Thus the pointer index of a pointer can change from one event to the next but + * the pointer id of a pointer is guaranteed to remain constant as long as the pointer + * remains active. Use the {@link #getPointerId(int)} method to obtain the + * pointer id of a pointer to track it across all subsequent motion events in a gesture. + * Then for successive motion events, use the {@link #findPointerIndex(int)} method + * to obtain the pointer index for a given pointer id in that motion event. + * </p><p> + * Mouse and stylus buttons can be retrieved using {@link #getButtonState()}. It is a + * good idea to check the button state while handling {@link #ACTION_DOWN} as part + * of a touch event. The application may choose to perform some different action + * if the touch event starts due to a secondary button click, such as presenting a + * context menu. + * </p> + * + * <h3>Batching</h3> + * <p> * For efficiency, motion events with {@link #ACTION_MOVE} may batch together * multiple movement samples within a single object. The most current * pointer coordinates are available using {@link #getX(int)} and {@link #getY(int)}. @@ -98,42 +105,104 @@ import android.util.SparseArray; * ev.getPointerId(p), ev.getX(p), ev.getY(p)); * } * } - * </code></pre></p><p> - * In general, the framework cannot guarantee that the motion events it delivers - * to a view always constitute a complete motion sequences since some events may be dropped - * or modified by containing views before they are delivered. The view implementation - * should be prepared to handle {@link #ACTION_CANCEL} and should tolerate anomalous - * situations such as receiving a new {@link #ACTION_DOWN} without first having - * received an {@link #ACTION_UP} for the prior gesture. + * </code></pre></p> + * + * <h3>Device Types</h3> + * <p> + * The interpretation of the contents of a MotionEvent varies significantly depending + * on the source class of the device. + * </p><p> + * On pointing devices with source class {@link InputDevice#SOURCE_CLASS_POINTER} + * such as touch screens, the pointer coordinates specify absolute + * positions such as view X/Y coordinates. Each complete gesture is represented + * by a sequence of motion events with actions that describe pointer state transitions + * and movements. A gesture starts with a motion event with {@link #ACTION_DOWN} + * that provides the location of the first pointer down. As each additional + * pointer that goes down or up, the framework will generate a motion event with + * {@link #ACTION_POINTER_DOWN} or {@link #ACTION_POINTER_UP} accordingly. + * Pointer movements are described by motion events with {@link #ACTION_MOVE}. + * Finally, a gesture end either when the final pointer goes up as represented + * by a motion event with {@link #ACTION_UP} or when gesture is canceled + * with {@link #ACTION_CANCEL}. + * </p><p> + * Some pointing devices such as mice may support vertical and/or horizontal scrolling. + * A scroll event is reported as a generic motion event with {@link #ACTION_SCROLL} that + * includes the relative scroll offset in the {@link #AXIS_VSCROLL} and + * {@link #AXIS_HSCROLL} axes. See {@link #getAxisValue(int)} for information + * about retrieving these additional axes. + * </p><p> + * On trackball devices with source class {@link InputDevice#SOURCE_CLASS_TRACKBALL}, + * the pointer coordinates specify relative movements as X/Y deltas. + * A trackball gesture consists of a sequence of movements described by motion + * events with {@link #ACTION_MOVE} interspersed with occasional {@link #ACTION_DOWN} + * or {@link #ACTION_UP} motion events when the trackball button is pressed or released. + * </p><p> + * On joystick devices with source class {@link InputDevice#SOURCE_CLASS_JOYSTICK}, + * the pointer coordinates specify the absolute position of the joystick axes. + * The joystick axis values are normalized to a range of -1.0 to 1.0 where 0.0 corresponds + * to the center position. More information about the set of available axes and the + * range of motion can be obtained using {@link InputDevice#getMotionRange}. + * Some common joystick axes are {@link #AXIS_X}, {@link #AXIS_Y}, + * {@link #AXIS_HAT_X}, {@link #AXIS_HAT_Y}, {@link #AXIS_Z} and {@link #AXIS_RZ}. * </p><p> * Refer to {@link InputDevice} for more information about how different kinds of * input devices and sources represent pointer coordinates. * </p> + * + * <h3>Consistency Guarantees</h3> + * <p> + * Motion events are always delivered to views as a consistent stream of events. + * What constitutes a consistent stream varies depending on the type of device. + * For touch events, consistency implies that pointers go down one at a time, + * move around as a group and then go up one at a time or are canceled. + * </p><p> + * While the framework tries to deliver consistent streams of motion events to + * views, it cannot guarantee it. Some events may be dropped or modified by + * containing views in the application before they are delivered thereby making + * the stream of events inconsistent. Views should always be prepared to + * handle {@link #ACTION_CANCEL} and should tolerate anomalous + * situations such as receiving a new {@link #ACTION_DOWN} without first having + * received an {@link #ACTION_UP} for the prior gesture. + * </p> */ public final class MotionEvent extends InputEvent implements Parcelable { private static final long NS_PER_MS = 1000000; private static final boolean TRACK_RECYCLED_LOCATION = false; - + + /** + * An invalid pointer id. + * + * This value (-1) can be used as a placeholder to indicate that a pointer id + * has not been assigned or is not available. It cannot appear as + * a pointer id inside a {@link MotionEvent}. + */ + public static final int INVALID_POINTER_ID = -1; + /** * Bit mask of the parts of the action code that are the action itself. */ public static final int ACTION_MASK = 0xff; /** - * Constant for {@link #getAction}: A pressed gesture has started, the + * Constant for {@link #getActionMasked}: A pressed gesture has started, the * motion contains the initial starting location. + * <p> + * This is also a good time to check the button state to distinguish + * secondary and tertiary button clicks and handle them appropriately. + * Use {@link #getButtonState} to retrieve the button state. + * </p> */ public static final int ACTION_DOWN = 0; /** - * Constant for {@link #getAction}: A pressed gesture has finished, the + * Constant for {@link #getActionMasked}: A pressed gesture has finished, the * motion contains the final release location as well as any intermediate * points since the last down or move event. */ public static final int ACTION_UP = 1; /** - * Constant for {@link #getAction}: A change has happened during a + * Constant for {@link #getActionMasked}: A change has happened during a * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}). * The motion contains the most recent point, as well as any intermediate * points since the last down or move event. @@ -141,37 +210,49 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int ACTION_MOVE = 2; /** - * Constant for {@link #getAction}: The current gesture has been aborted. + * Constant for {@link #getActionMasked}: The current gesture has been aborted. * You will not receive any more points in it. You should treat this as * an up event, but not perform any action that you normally would. */ public static final int ACTION_CANCEL = 3; /** - * Constant for {@link #getAction}: A movement has happened outside of the + * Constant for {@link #getActionMasked}: A movement has happened outside of the * normal bounds of the UI element. This does not provide a full gesture, * but only the initial location of the movement/touch. */ public static final int ACTION_OUTSIDE = 4; /** - * A non-primary pointer has gone down. The bits in - * {@link #ACTION_POINTER_ID_MASK} indicate which pointer changed. + * Constant for {@link #getActionMasked}: A non-primary pointer has gone down. + * <p> + * Use {@link #getActionIndex} to retrieve the index of the pointer that changed. + * </p><p> + * The index is encoded in the {@link #ACTION_POINTER_INDEX_MASK} bits of the + * unmasked action returned by {@link #getAction}. + * </p> */ public static final int ACTION_POINTER_DOWN = 5; /** - * A non-primary pointer has gone up. The bits in - * {@link #ACTION_POINTER_ID_MASK} indicate which pointer changed. + * Constant for {@link #getActionMasked}: A non-primary pointer has gone up. + * <p> + * Use {@link #getActionIndex} to retrieve the index of the pointer that changed. + * </p><p> + * The index is encoded in the {@link #ACTION_POINTER_INDEX_MASK} bits of the + * unmasked action returned by {@link #getAction}. + * </p> */ public static final int ACTION_POINTER_UP = 6; /** - * Constant for {@link #getAction}: A change happened but the pointer + * Constant for {@link #getActionMasked}: A change happened but the pointer * is not down (unlike {@link #ACTION_MOVE}). The motion contains the most * recent point, as well as any intermediate points since the last * hover move event. * <p> + * This action is always delivered to the window or view under the pointer. + * </p><p> * This action is not a touch event so it is delivered to * {@link View#onGenericMotionEvent(MotionEvent)} rather than * {@link View#onTouchEvent(MotionEvent)}. @@ -180,13 +261,14 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int ACTION_HOVER_MOVE = 7; /** - * Constant for {@link #getAction}: The motion event contains relative + * Constant for {@link #getActionMasked}: The motion event contains relative * vertical and/or horizontal scroll offsets. Use {@link #getAxisValue(int)} * to retrieve the information from {@link #AXIS_VSCROLL} and {@link #AXIS_HSCROLL}. * The pointer may or may not be down when this event is dispatched. - * This action is always delivered to the winder under the pointer, which - * may not be the window currently touched. * <p> + * This action is always delivered to the window or view under the pointer, which + * may not be the window or view currently touched. + * </p><p> * This action is not a touch event so it is delivered to * {@link View#onGenericMotionEvent(MotionEvent)} rather than * {@link View#onTouchEvent(MotionEvent)}. @@ -195,21 +277,51 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int ACTION_SCROLL = 8; /** + * Constant for {@link #getActionMasked}: The pointer is not down but has entered the + * boundaries of a window or view. + * <p> + * This action is always delivered to the window or view under the pointer. + * </p><p> + * This action is not a touch event so it is delivered to + * {@link View#onGenericMotionEvent(MotionEvent)} rather than + * {@link View#onTouchEvent(MotionEvent)}. + * </p> + */ + public static final int ACTION_HOVER_ENTER = 9; + + /** + * Constant for {@link #getActionMasked}: The pointer is not down but has exited the + * boundaries of a window or view. + * <p> + * This action is always delivered to the window or view that was previously under the pointer. + * </p><p> + * This action is not a touch event so it is delivered to + * {@link View#onGenericMotionEvent(MotionEvent)} rather than + * {@link View#onTouchEvent(MotionEvent)}. + * </p> + */ + public static final int ACTION_HOVER_EXIT = 10; + + /** * Bits in the action code that represent a pointer index, used with * {@link #ACTION_POINTER_DOWN} and {@link #ACTION_POINTER_UP}. Shifting * down by {@link #ACTION_POINTER_INDEX_SHIFT} provides the actual pointer * index where the data for the pointer going up or down can be found; you can * get its identifier with {@link #getPointerId(int)} and the actual * data with {@link #getX(int)} etc. + * + * @see #getActionIndex */ public static final int ACTION_POINTER_INDEX_MASK = 0xff00; /** * Bit shift for the action bits holding the pointer index as * defined by {@link #ACTION_POINTER_INDEX_MASK}. + * + * @see #getActionIndex */ public static final int ACTION_POINTER_INDEX_SHIFT = 8; - + /** * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the * data index associated with {@link #ACTION_POINTER_DOWN}. @@ -279,6 +391,17 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int FLAG_WINDOW_IS_OBSCURED = 0x1; /** + * Private flag that indicates when the system has detected that this motion event + * may be inconsistent with respect to the sequence of previously delivered motion events, + * such as when a pointer move event is sent but the pointer is not down. + * + * @hide + * @see #isTainted + * @see #setTainted + */ + public static final int FLAG_TAINTED = 0x80000000; + + /** * Flag indicating the motion event intersected the top edge of the screen. */ public static final int EDGE_TOP = 0x00000001; @@ -299,7 +422,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int EDGE_RIGHT = 0x00000008; /** - * Constant used to identify the X axis of a motion event. + * Axis constant: X axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the absolute X screen position of the center of @@ -324,7 +447,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_X = 0; /** - * Constant used to identify the Y axis of a motion event. + * Axis constant: Y axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the absolute Y screen position of the center of @@ -349,7 +472,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_Y = 1; /** - * Constant used to identify the Pressure axis of a motion event. + * Axis constant: Pressure axis of a motion event. * <p> * <ul> * <li>For a touch screen or touch pad, reports the approximate pressure applied to the surface @@ -371,7 +494,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_PRESSURE = 2; /** - * Constant used to identify the Size axis of a motion event. + * Axis constant: Size axis of a motion event. * <p> * <ul> * <li>For a touch screen or touch pad, reports the approximate size of the contact area in @@ -391,7 +514,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_SIZE = 3; /** - * Constant used to identify the TouchMajor axis of a motion event. + * Axis constant: TouchMajor axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the length of the major axis of an ellipse that @@ -412,7 +535,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_TOUCH_MAJOR = 4; /** - * Constant used to identify the TouchMinor axis of a motion event. + * Axis constant: TouchMinor axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the length of the minor axis of an ellipse that @@ -435,7 +558,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_TOUCH_MINOR = 5; /** - * Constant used to identify the ToolMajor axis of a motion event. + * Axis constant: ToolMajor axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the length of the major axis of an ellipse that @@ -460,7 +583,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_TOOL_MAJOR = 6; /** - * Constant used to identify the ToolMinor axis of a motion event. + * Axis constant: ToolMinor axis of a motion event. * <p> * <ul> * <li>For a touch screen, reports the length of the minor axis of an ellipse that @@ -485,7 +608,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_TOOL_MINOR = 7; /** - * Constant used to identify the Orientation axis of a motion event. + * Axis constant: Orientation axis of a motion event. * <p> * <ul> * <li>For a touch screen or touch pad, reports the orientation of the finger @@ -507,7 +630,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_ORIENTATION = 8; /** - * Constant used to identify the Vertical Scroll axis of a motion event. + * Axis constant: Vertical Scroll axis of a motion event. * <p> * <ul> * <li>For a mouse, reports the relative movement of the vertical scroll wheel. @@ -525,7 +648,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_VSCROLL = 9; /** - * Constant used to identify the Horizontal Scroll axis of a motion event. + * Axis constant: Horizontal Scroll axis of a motion event. * <p> * <ul> * <li>For a mouse, reports the relative movement of the horizontal scroll wheel. @@ -543,7 +666,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_HSCROLL = 10; /** - * Constant used to identify the Z axis of a motion event. + * Axis constant: Z axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute Z position of the joystick. @@ -561,7 +684,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_Z = 11; /** - * Constant used to identify the X Rotation axis of a motion event. + * Axis constant: X Rotation axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute rotation angle about the X axis. @@ -577,7 +700,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_RX = 12; /** - * Constant used to identify the Y Rotation axis of a motion event. + * Axis constant: Y Rotation axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute rotation angle about the Y axis. @@ -593,7 +716,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_RY = 13; /** - * Constant used to identify the Z Rotation axis of a motion event. + * Axis constant: Z Rotation axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute rotation angle about the Z axis. @@ -611,7 +734,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_RZ = 14; /** - * Constant used to identify the Hat X axis of a motion event. + * Axis constant: Hat X axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute X position of the directional hat control. @@ -627,7 +750,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_HAT_X = 15; /** - * Constant used to identify the Hat Y axis of a motion event. + * Axis constant: Hat Y axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute Y position of the directional hat control. @@ -643,7 +766,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_HAT_Y = 16; /** - * Constant used to identify the Left Trigger axis of a motion event. + * Axis constant: Left Trigger axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the left trigger control. @@ -659,7 +782,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_LTRIGGER = 17; /** - * Constant used to identify the Right Trigger axis of a motion event. + * Axis constant: Right Trigger axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the right trigger control. @@ -675,7 +798,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_RTRIGGER = 18; /** - * Constant used to identify the Throttle axis of a motion event. + * Axis constant: Throttle axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the throttle control. @@ -691,7 +814,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_THROTTLE = 19; /** - * Constant used to identify the Rudder axis of a motion event. + * Axis constant: Rudder axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the rudder control. @@ -707,7 +830,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_RUDDER = 20; /** - * Constant used to identify the Wheel axis of a motion event. + * Axis constant: Wheel axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the steering wheel control. @@ -723,7 +846,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_WHEEL = 21; /** - * Constant used to identify the Gas axis of a motion event. + * Axis constant: Gas axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the gas (accelerator) control. @@ -740,7 +863,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GAS = 22; /** - * Constant used to identify the Brake axis of a motion event. + * Axis constant: Brake axis of a motion event. * <p> * <ul> * <li>For a joystick, reports the absolute position of the brake control. @@ -756,7 +879,24 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_BRAKE = 23; /** - * Constant used to identify the Generic 1 axis of a motion event. + * Axis constant: Distance axis of a motion event. + * <p> + * <ul> + * <li>For a stylus, reports the distance of the stylus from the screen. + * The value is nominally measured in millimeters where 0.0 indicates direct contact + * and larger values indicate increasing distance from the surface. + * </ul> + * </p> + * + * @see #getAxisValue(int, int) + * @see #getHistoricalAxisValue(int, int, int) + * @see MotionEvent.PointerCoords#getAxisValue(int) + * @see InputDevice#getMotionRange + */ + public static final int AXIS_DISTANCE = 24; + + /** + * Axis constant: Generic 1 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -767,7 +907,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_1 = 32; /** - * Constant used to identify the Generic 2 axis of a motion event. + * Axis constant: Generic 2 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -778,7 +918,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_2 = 33; /** - * Constant used to identify the Generic 3 axis of a motion event. + * Axis constant: Generic 3 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -789,7 +929,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_3 = 34; /** - * Constant used to identify the Generic 4 axis of a motion event. + * Axis constant: Generic 4 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -800,7 +940,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_4 = 35; /** - * Constant used to identify the Generic 5 axis of a motion event. + * Axis constant: Generic 5 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -811,7 +951,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_5 = 36; /** - * Constant used to identify the Generic 6 axis of a motion event. + * Axis constant: Generic 6 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -822,7 +962,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_6 = 37; /** - * Constant used to identify the Generic 7 axis of a motion event. + * Axis constant: Generic 7 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -833,7 +973,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_7 = 38; /** - * Constant used to identify the Generic 8 axis of a motion event. + * Axis constant: Generic 8 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -844,7 +984,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_8 = 39; /** - * Constant used to identify the Generic 9 axis of a motion event. + * Axis constant: Generic 9 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -855,7 +995,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_9 = 40; /** - * Constant used to identify the Generic 10 axis of a motion event. + * Axis constant: Generic 10 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -866,7 +1006,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_10 = 41; /** - * Constant used to identify the Generic 11 axis of a motion event. + * Axis constant: Generic 11 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -877,7 +1017,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_11 = 42; /** - * Constant used to identify the Generic 12 axis of a motion event. + * Axis constant: Generic 12 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -888,7 +1028,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_12 = 43; /** - * Constant used to identify the Generic 13 axis of a motion event. + * Axis constant: Generic 13 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -899,7 +1039,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_13 = 44; /** - * Constant used to identify the Generic 14 axis of a motion event. + * Axis constant: Generic 14 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -910,7 +1050,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_14 = 45; /** - * Constant used to identify the Generic 15 axis of a motion event. + * Axis constant: Generic 15 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -921,7 +1061,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static final int AXIS_GENERIC_15 = 46; /** - * Constant used to identify the Generic 16 axis of a motion event. + * Axis constant: Generic 16 axis of a motion event. * The interpretation of a generic axis is device-specific. * * @see #getAxisValue(int, int) @@ -937,7 +1077,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { // Symbolic names of all axes. private static final SparseArray<String> AXIS_SYMBOLIC_NAMES = new SparseArray<String>(); - private static void populateAxisSymbolicNames() { + static { SparseArray<String> names = AXIS_SYMBOLIC_NAMES; names.append(AXIS_X, "AXIS_X"); names.append(AXIS_Y, "AXIS_Y"); @@ -963,6 +1103,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { names.append(AXIS_WHEEL, "AXIS_WHEEL"); names.append(AXIS_GAS, "AXIS_GAS"); names.append(AXIS_BRAKE, "AXIS_BRAKE"); + names.append(AXIS_DISTANCE, "AXIS_DISTANCE"); names.append(AXIS_GENERIC_1, "AXIS_GENERIC_1"); names.append(AXIS_GENERIC_2, "AXIS_GENERIC_2"); names.append(AXIS_GENERIC_3, "AXIS_GENERIC_3"); @@ -981,8 +1122,169 @@ public final class MotionEvent extends InputEvent implements Parcelable { names.append(AXIS_GENERIC_16, "AXIS_GENERIC_16"); } + /** + * Button constant: Primary button (left mouse button, stylus tip). + * + * @see #getButtonState + */ + public static final int BUTTON_PRIMARY = 1 << 0; + + /** + * Button constant: Secondary button (right mouse button, stylus barrel). + * + * @see #getButtonState + */ + public static final int BUTTON_SECONDARY = 1 << 1; + + /** + * Button constant: Tertiary button (middle mouse button). + * + * @see #getButtonState + */ + public static final int BUTTON_TERTIARY = 1 << 2; + + /** + * Button constant: Back button pressed (mouse back button). + * <p> + * The system may send a {@link KeyEvent#KEYCODE_BACK} key press to the application + * when this button is pressed. + * </p> + * + * @see #getButtonState + */ + public static final int BUTTON_BACK = 1 << 3; + + /** + * Button constant: Forward button pressed (mouse forward button). + * <p> + * The system may send a {@link KeyEvent#KEYCODE_FORWARD} key press to the application + * when this button is pressed. + * </p> + * + * @see #getButtonState + */ + public static final int BUTTON_FORWARD = 1 << 4; + + /** + * Button constant: Eraser button pressed (stylus end). + * + * @see #getButtonState + */ + public static final int BUTTON_ERASER = 1 << 5; + + // NOTE: If you add a new axis here you must also add it to: + // native/include/android/input.h + + // Symbolic names of all button states in bit order from least significant + // to most significant. + private static final String[] BUTTON_SYMBOLIC_NAMES = new String[] { + "BUTTON_PRIMARY", + "BUTTON_SECONDARY", + "BUTTON_TERTIARY", + "BUTTON_BACK", + "BUTTON_FORWARD", + "BUTTON_ERASER", + "0x00000040", + "0x00000080", + "0x00000100", + "0x00000200", + "0x00000400", + "0x00000800", + "0x00001000", + "0x00002000", + "0x00004000", + "0x00008000", + "0x00010000", + "0x00020000", + "0x00040000", + "0x00080000", + "0x00100000", + "0x00200000", + "0x00400000", + "0x00800000", + "0x01000000", + "0x02000000", + "0x04000000", + "0x08000000", + "0x10000000", + "0x20000000", + "0x40000000", + "0x80000000", + }; + + /** + * Tool type constant: Unknown tool type. + * This constant is used when the tool type is not known or is not relevant, + * such as for a trackball or other non-pointing device. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_UNKNOWN = 0; + + /** + * Tool type constant: The tool is a finger directly touching the display. + * + * This is a <em>direct</em> positioning tool. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_FINGER = 1; + + /** + * Tool type constant: The tool is a stylus directly touching the display + * or hovering slightly above it. + * + * This is a <em>direct</em> positioning tool. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_STYLUS = 2; + + /** + * Tool type constant: The tool is a mouse or trackpad that translates + * relative motions into cursor movements on the display. + * + * This is an <em>indirect</em> positioning tool. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_MOUSE = 3; + + /** + * Tool type constant: The tool is a finger on a touch pad that is not + * directly attached to the display. Finger movements on the touch pad + * may be translated into touches on the display, possibly with visual feedback. + * + * This is an <em>indirect</em> positioning tool. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_INDIRECT_FINGER = 4; + + /** + * Tool type constant: The tool is a stylus on a digitizer tablet that is not + * attached to the display. Stylus movements on the digitizer may be translated + * into touches on the display, possibly with visual feedback. + * + * This is an <em>indirect</em> positioning tool. + * + * @see #getToolType + */ + public static final int TOOL_TYPE_INDIRECT_STYLUS = 5; + + // NOTE: If you add a new tool type here you must also add it to: + // native/include/android/input.h + + // Symbolic names of all tool types. + private static final SparseArray<String> TOOL_TYPE_SYMBOLIC_NAMES = new SparseArray<String>(); static { - populateAxisSymbolicNames(); + SparseArray<String> names = TOOL_TYPE_SYMBOLIC_NAMES; + names.append(TOOL_TYPE_UNKNOWN, "TOOL_TYPE_UNKNOWN"); + names.append(TOOL_TYPE_FINGER, "TOOL_TYPE_FINGER"); + names.append(TOOL_TYPE_STYLUS, "TOOL_TYPE_STYLUS"); + names.append(TOOL_TYPE_MOUSE, "TOOL_TYPE_MOUSE"); + names.append(TOOL_TYPE_INDIRECT_FINGER, "TOOL_TYPE_INDIRECT_FINGER"); + names.append(TOOL_TYPE_INDIRECT_STYLUS, "TOOL_TYPE_INDIRECT_STYLUS"); } // Private value for history pos that obtains the current sample. @@ -995,10 +1297,23 @@ public final class MotionEvent extends InputEvent implements Parcelable { // Shared temporary objects used when translating coordinates supplied by // the caller into single element PointerCoords and pointer id arrays. - // Must lock gTmpPointerCoords prior to use. - private static final PointerCoords[] gTmpPointerCoords = - new PointerCoords[] { new PointerCoords() }; - private static final int[] gTmpPointerIds = new int[] { 0 /*always 0*/ }; + private static final Object gSharedTempLock = new Object(); + private static PointerCoords[] gSharedTempPointerCoords; + private static PointerProperties[] gSharedTempPointerProperties; + private static int[] gSharedTempPointerIndexMap; + + private static final void ensureSharedTempPointerCapacity(int desiredCapacity) { + if (gSharedTempPointerCoords == null + || gSharedTempPointerCoords.length < desiredCapacity) { + int capacity = gSharedTempPointerCoords != null ? gSharedTempPointerCoords.length : 8; + while (capacity < desiredCapacity) { + capacity *= 2; + } + gSharedTempPointerCoords = PointerCoords.createArray(capacity); + gSharedTempPointerProperties = PointerProperties.createArray(capacity); + gSharedTempPointerIndexMap = new int[capacity]; + } + } // Pointer to the native MotionEvent object that contains the actual data. private int mNativePtr; @@ -1008,10 +1323,11 @@ public final class MotionEvent extends InputEvent implements Parcelable { private boolean mRecycled; private static native int nativeInitialize(int nativePtr, - int deviceId, int source, int action, int flags, int edgeFlags, int metaState, + int deviceId, int source, int action, int flags, int edgeFlags, + int metaState, int buttonState, float xOffset, float yOffset, float xPrecision, float yPrecision, long downTimeNanos, long eventTimeNanos, - int pointerCount, int[] pointerIds, PointerCoords[] pointerCoords); + int pointerCount, PointerProperties[] pointerIds, PointerCoords[] pointerCoords); private static native int nativeCopy(int destNativePtr, int sourceNativePtr, boolean keepHistory); private static native void nativeDispose(int nativePtr); @@ -1025,16 +1341,22 @@ public final class MotionEvent extends InputEvent implements Parcelable { private static native void nativeSetAction(int nativePtr, int action); private static native boolean nativeIsTouchEvent(int nativePtr); private static native int nativeGetFlags(int nativePtr); + private static native void nativeSetFlags(int nativePtr, int flags); private static native int nativeGetEdgeFlags(int nativePtr); private static native void nativeSetEdgeFlags(int nativePtr, int action); private static native int nativeGetMetaState(int nativePtr); + private static native int nativeGetButtonState(int nativePtr); private static native void nativeOffsetLocation(int nativePtr, float deltaX, float deltaY); + private static native float nativeGetXOffset(int nativePtr); + private static native float nativeGetYOffset(int nativePtr); private static native float nativeGetXPrecision(int nativePtr); private static native float nativeGetYPrecision(int nativePtr); private static native long nativeGetDownTimeNanos(int nativePtr); + private static native void nativeSetDownTimeNanos(int nativePtr, long downTime); private static native int nativeGetPointerCount(int nativePtr); private static native int nativeGetPointerId(int nativePtr, int pointerIndex); + private static native int nativeGetToolType(int nativePtr, int pointerIndex); private static native int nativeFindPointerIndex(int nativePtr, int pointerId); private static native int nativeGetHistorySize(int nativePtr); @@ -1045,6 +1367,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { int axis, int pointerIndex, int historyPos); private static native void nativeGetPointerCoords(int nativePtr, int pointerIndex, int historyPos, PointerCoords outPointerCoords); + private static native void nativeGetPointerProperties(int nativePtr, + int pointerIndex, PointerProperties outPointerProperties); private static native void nativeScale(int nativePtr, float scale); private static native void nativeTransform(int nativePtr, Matrix matrix); @@ -1086,19 +1410,21 @@ public final class MotionEvent extends InputEvent implements Parcelable { /** * Create a new MotionEvent, filling in all of the basic values that * define the motion. - * + * * @param downTime The time (in ms) when the user originally pressed down to start * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}. * @param eventTime The the time (in ms) when this specific event was generated. This * must be obtained from {@link SystemClock#uptimeMillis()}. * @param action The kind of action being performed, such as {@link #ACTION_DOWN}. - * @param pointers The number of points that will be in this event. - * @param pointerIds An array of <em>pointers</em> values providing - * an identifier for each pointer. - * @param pointerCoords An array of <em>pointers</em> values providing + * @param pointerCount The number of pointers that will be in this event. + * @param pointerProperties An array of <em>pointerCount</em> values providing + * a {@link PointerProperties} property object for each pointer, which must + * include the pointer identifier. + * @param pointerCoords An array of <em>pointerCount</em> values providing * a {@link PointerCoords} coordinate object for each pointer. * @param metaState The state of any meta / modifier keys that were in effect when * the event was generated. + * @param buttonState The state of buttons that are pressed. * @param xPrecision The precision of the X coordinate being reported. * @param yPrecision The precision of the Y coordinate being reported. * @param deviceId The id for the device that this event came from. An id of @@ -1110,21 +1436,69 @@ public final class MotionEvent extends InputEvent implements Parcelable { * @param flags The motion event flags. */ static public MotionEvent obtain(long downTime, long eventTime, - int action, int pointers, int[] pointerIds, PointerCoords[] pointerCoords, - int metaState, float xPrecision, float yPrecision, int deviceId, + int action, int pointerCount, PointerProperties[] pointerProperties, + PointerCoords[] pointerCoords, int metaState, int buttonState, + float xPrecision, float yPrecision, int deviceId, int edgeFlags, int source, int flags) { MotionEvent ev = obtain(); ev.mNativePtr = nativeInitialize(ev.mNativePtr, - deviceId, source, action, flags, edgeFlags, metaState, + deviceId, source, action, flags, edgeFlags, metaState, buttonState, 0, 0, xPrecision, yPrecision, downTime * NS_PER_MS, eventTime * NS_PER_MS, - pointers, pointerIds, pointerCoords); + pointerCount, pointerProperties, pointerCoords); return ev; } /** * Create a new MotionEvent, filling in all of the basic values that * define the motion. + * + * @param downTime The time (in ms) when the user originally pressed down to start + * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}. + * @param eventTime The the time (in ms) when this specific event was generated. This + * must be obtained from {@link SystemClock#uptimeMillis()}. + * @param action The kind of action being performed, such as {@link #ACTION_DOWN}. + * @param pointerCount The number of pointers that will be in this event. + * @param pointerIds An array of <em>pointerCount</em> values providing + * an identifier for each pointer. + * @param pointerCoords An array of <em>pointerCount</em> values providing + * a {@link PointerCoords} coordinate object for each pointer. + * @param metaState The state of any meta / modifier keys that were in effect when + * the event was generated. + * @param xPrecision The precision of the X coordinate being reported. + * @param yPrecision The precision of the Y coordinate being reported. + * @param deviceId The id for the device that this event came from. An id of + * zero indicates that the event didn't come from a physical device; other + * numbers are arbitrary and you shouldn't depend on the values. + * @param edgeFlags A bitfield indicating which edges, if any, were touched by this + * MotionEvent. + * @param source The source of this event. + * @param flags The motion event flags. + * + * @deprecated Use {@link #obtain(long, long, int, int, PointerProperties[], PointerCoords[], int, int, float, float, int, int, int, int)} + * instead. + */ + @Deprecated + static public MotionEvent obtain(long downTime, long eventTime, + int action, int pointerCount, int[] pointerIds, PointerCoords[] pointerCoords, + int metaState, float xPrecision, float yPrecision, int deviceId, + int edgeFlags, int source, int flags) { + synchronized (gSharedTempLock) { + ensureSharedTempPointerCapacity(pointerCount); + final PointerProperties[] pp = gSharedTempPointerProperties; + for (int i = 0; i < pointerCount; i++) { + pp[i].clear(); + pp[i].id = pointerIds[i]; + } + return obtain(downTime, eventTime, action, pointerCount, pp, + pointerCoords, metaState, 0, xPrecision, yPrecision, deviceId, + edgeFlags, source, flags); + } + } + + /** + * Create a new MotionEvent, filling in all of the basic values that + * define the motion. * * @param downTime The time (in ms) when the user originally pressed down to start * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}. @@ -1154,20 +1528,25 @@ public final class MotionEvent extends InputEvent implements Parcelable { static public MotionEvent obtain(long downTime, long eventTime, int action, float x, float y, float pressure, float size, int metaState, float xPrecision, float yPrecision, int deviceId, int edgeFlags) { - synchronized (gTmpPointerCoords) { - final PointerCoords pc = gTmpPointerCoords[0]; - pc.clear(); - pc.x = x; - pc.y = y; - pc.pressure = pressure; - pc.size = size; - - MotionEvent ev = obtain(); + MotionEvent ev = obtain(); + synchronized (gSharedTempLock) { + ensureSharedTempPointerCapacity(1); + final PointerProperties[] pp = gSharedTempPointerProperties; + pp[0].clear(); + pp[0].id = 0; + + final PointerCoords pc[] = gSharedTempPointerCoords; + pc[0].clear(); + pc[0].x = x; + pc[0].y = y; + pc[0].pressure = pressure; + pc[0].size = size; + ev.mNativePtr = nativeInitialize(ev.mNativePtr, - deviceId, InputDevice.SOURCE_UNKNOWN, action, 0, edgeFlags, metaState, + deviceId, InputDevice.SOURCE_UNKNOWN, action, 0, edgeFlags, metaState, 0, 0, 0, xPrecision, yPrecision, downTime * NS_PER_MS, eventTime * NS_PER_MS, - 1, gTmpPointerIds, gTmpPointerCoords); + 1, pp, pc); return ev; } } @@ -1181,7 +1560,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { * @param eventTime The the time (in ms) when this specific event was generated. This * must be obtained from {@link SystemClock#uptimeMillis()}. * @param action The kind of action being performed, such as {@link #ACTION_DOWN}. - * @param pointers The number of pointers that are active in this event. + * @param pointerCount The number of pointers that are active in this event. * @param x The X coordinate of this event. * @param y The Y coordinate of this event. * @param pressure The current pressure of this event. The pressure generally @@ -1207,7 +1586,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { */ @Deprecated static public MotionEvent obtain(long downTime, long eventTime, int action, - int pointers, float x, float y, float pressure, float size, int metaState, + int pointerCount, float x, float y, float pressure, float size, int metaState, float xPrecision, float yPrecision, int deviceId, int edgeFlags) { return obtain(downTime, eventTime, action, x, y, pressure, size, metaState, xPrecision, yPrecision, deviceId, edgeFlags); @@ -1261,6 +1640,12 @@ public final class MotionEvent extends InputEvent implements Parcelable { return ev; } + /** @hide */ + @Override + public MotionEvent copy() { + return obtain(this); + } + /** * Recycle the MotionEvent, to be re-used by a later caller. After calling * this function you must not ever touch the event again. @@ -1354,9 +1739,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { /** * Returns true if this motion event is a touch event. * <p> - * Specifically excludes pointer events with action {@link #ACTION_HOVER_MOVE} - * or {@link #ACTION_SCROLL} because they are not actually touch events - * (the pointer is not down). + * Specifically excludes pointer events with action {@link #ACTION_HOVER_MOVE}, + * {@link #ACTION_HOVER_ENTER}, {@link #ACTION_HOVER_EXIT}, or {@link #ACTION_SCROLL} + * because they are not actually touch events (the pointer is not down). * </p> * @return True if this motion event is a touch event. * @hide @@ -1374,6 +1759,20 @@ public final class MotionEvent extends InputEvent implements Parcelable { return nativeGetFlags(mNativePtr); } + /** @hide */ + @Override + public final boolean isTainted() { + final int flags = getFlags(); + return (flags & FLAG_TAINTED) != 0; + } + + /** @hide */ + @Override + public final void setTainted(boolean tainted) { + final int flags = getFlags(); + nativeSetFlags(mNativePtr, tainted ? flags | FLAG_TAINTED : flags & ~FLAG_TAINTED); + } + /** * Returns the time (in ms) when the user originally pressed down to start * a stream of position events. @@ -1383,6 +1782,16 @@ public final class MotionEvent extends InputEvent implements Parcelable { } /** + * Sets the time (in ms) when the user originally pressed down to start + * a stream of position events. + * + * @hide + */ + public final void setDownTime(long downTime) { + nativeSetDownTimeNanos(mNativePtr, downTime * NS_PER_MS); + } + + /** * Returns the time (in ms) when this specific event was generated. */ public final long getEventTime() { @@ -1521,7 +1930,27 @@ public final class MotionEvent extends InputEvent implements Parcelable { public final int getPointerId(int pointerIndex) { return nativeGetPointerId(mNativePtr, pointerIndex); } - + + /** + * Gets the tool type of a pointer for the given pointer index. + * The tool type indicates the type of tool used to make contact such + * as a finger or stylus, if known. + * + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. + * @return The tool type of the pointer. + * + * @see #TOOL_TYPE_UNKNOWN + * @see #TOOL_TYPE_FINGER + * @see #TOOL_TYPE_STYLUS + * @see #TOOL_TYPE_MOUSE + * @see #TOOL_TYPE_INDIRECT_FINGER + * @see #TOOL_TYPE_INDIRECT_STYLUS + */ + public final int getToolType(int pointerIndex) { + return nativeGetToolType(mNativePtr, pointerIndex); + } + /** * Given a pointer identifier, find the index of its data in the event. * @@ -1533,7 +1962,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { public final int findPointerIndex(int pointerId) { return nativeFindPointerIndex(mNativePtr, pointerId); } - + /** * Returns the X coordinate of this event for the given pointer * <em>index</em> (use {@link #getPointerId(int)} to find the pointer @@ -1709,6 +2138,21 @@ public final class MotionEvent extends InputEvent implements Parcelable { } /** + * Populates a {@link PointerProperties} object with pointer properties for + * the specified pointer index. + * + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount()}-1. + * @param outPointerProperties The pointer properties object to populate. + * + * @see PointerProperties + */ + public final void getPointerProperties(int pointerIndex, + PointerProperties outPointerProperties) { + nativeGetPointerProperties(mNativePtr, pointerIndex, outPointerProperties); + } + + /** * Returns the state of any meta / modifier keys that were in effect when * the event was generated. This is the same values as those * returned by {@link KeyEvent#getMetaState() KeyEvent.getMetaState}. @@ -1723,6 +2167,22 @@ public final class MotionEvent extends InputEvent implements Parcelable { } /** + * Gets the state of all buttons that are pressed such as a mouse or stylus button. + * + * @return The button state. + * + * @see #BUTTON_PRIMARY + * @see #BUTTON_SECONDARY + * @see #BUTTON_TERTIARY + * @see #BUTTON_FORWARD + * @see #BUTTON_BACK + * @see #BUTTON_ERASER + */ + public final int getButtonState() { + return nativeGetButtonState(mNativePtr); + } + + /** * Returns the original raw X coordinate of this event. For touch * events on the screen, this is the original location of the event * on the screen, before it had been adjusted for the containing window @@ -2236,14 +2696,16 @@ public final class MotionEvent extends InputEvent implements Parcelable { */ public final void addBatch(long eventTime, float x, float y, float pressure, float size, int metaState) { - synchronized (gTmpPointerCoords) { - final PointerCoords pc = gTmpPointerCoords[0]; - pc.clear(); - pc.x = x; - pc.y = y; - pc.pressure = pressure; - pc.size = size; - nativeAddBatch(mNativePtr, eventTime * NS_PER_MS, gTmpPointerCoords, metaState); + synchronized (gSharedTempLock) { + ensureSharedTempPointerCapacity(1); + final PointerCoords[] pc = gSharedTempPointerCoords; + pc[0].clear(); + pc[0].x = x; + pc[0].y = y; + pc[0].pressure = pressure; + pc[0].size = size; + + nativeAddBatch(mNativePtr, eventTime * NS_PER_MS, pc, metaState); } } @@ -2262,30 +2724,187 @@ public final class MotionEvent extends InputEvent implements Parcelable { nativeAddBatch(mNativePtr, eventTime * NS_PER_MS, pointerCoords, metaState); } + /** + * Returns true if all points in the motion event are completely within the specified bounds. + * @hide + */ + public final boolean isWithinBoundsNoHistory(float left, float top, + float right, float bottom) { + final int pointerCount = nativeGetPointerCount(mNativePtr); + for (int i = 0; i < pointerCount; i++) { + final float x = nativeGetAxisValue(mNativePtr, AXIS_X, i, HISTORY_CURRENT); + final float y = nativeGetAxisValue(mNativePtr, AXIS_Y, i, HISTORY_CURRENT); + if (x < left || x > right || y < top || y > bottom) { + return false; + } + } + return true; + } + + private static final float clamp(float value, float low, float high) { + if (value < low) { + return low; + } else if (value > high) { + return high; + } + return value; + } + + /** + * Returns a new motion events whose points have been clamped to the specified bounds. + * @hide + */ + public final MotionEvent clampNoHistory(float left, float top, float right, float bottom) { + MotionEvent ev = obtain(); + synchronized (gSharedTempLock) { + final int pointerCount = nativeGetPointerCount(mNativePtr); + + ensureSharedTempPointerCapacity(pointerCount); + final PointerProperties[] pp = gSharedTempPointerProperties; + final PointerCoords[] pc = gSharedTempPointerCoords; + + for (int i = 0; i < pointerCount; i++) { + nativeGetPointerProperties(mNativePtr, i, pp[i]); + nativeGetPointerCoords(mNativePtr, i, HISTORY_CURRENT, pc[i]); + pc[i].x = clamp(pc[i].x, left, right); + pc[i].y = clamp(pc[i].y, top, bottom); + } + ev.mNativePtr = nativeInitialize(ev.mNativePtr, + nativeGetDeviceId(mNativePtr), nativeGetSource(mNativePtr), + nativeGetAction(mNativePtr), nativeGetFlags(mNativePtr), + nativeGetEdgeFlags(mNativePtr), nativeGetMetaState(mNativePtr), + nativeGetButtonState(mNativePtr), + nativeGetXOffset(mNativePtr), nativeGetYOffset(mNativePtr), + nativeGetXPrecision(mNativePtr), nativeGetYPrecision(mNativePtr), + nativeGetDownTimeNanos(mNativePtr), + nativeGetEventTimeNanos(mNativePtr, HISTORY_CURRENT), + pointerCount, pp, pc); + return ev; + } + } + + /** + * Gets an integer where each pointer id present in the event is marked as a bit. + * @hide + */ + public final int getPointerIdBits() { + int idBits = 0; + final int pointerCount = nativeGetPointerCount(mNativePtr); + for (int i = 0; i < pointerCount; i++) { + idBits |= 1 << nativeGetPointerId(mNativePtr, i); + } + return idBits; + } + + /** + * Splits a motion event such that it includes only a subset of pointer ids. + * @hide + */ + public final MotionEvent split(int idBits) { + MotionEvent ev = obtain(); + synchronized (gSharedTempLock) { + final int oldPointerCount = nativeGetPointerCount(mNativePtr); + ensureSharedTempPointerCapacity(oldPointerCount); + final PointerProperties[] pp = gSharedTempPointerProperties; + final PointerCoords[] pc = gSharedTempPointerCoords; + final int[] map = gSharedTempPointerIndexMap; + + final int oldAction = nativeGetAction(mNativePtr); + final int oldActionMasked = oldAction & ACTION_MASK; + final int oldActionPointerIndex = (oldAction & ACTION_POINTER_INDEX_MASK) + >> ACTION_POINTER_INDEX_SHIFT; + int newActionPointerIndex = -1; + int newPointerCount = 0; + int newIdBits = 0; + for (int i = 0; i < oldPointerCount; i++) { + nativeGetPointerProperties(mNativePtr, i, pp[newPointerCount]); + final int idBit = 1 << pp[newPointerCount].id; + if ((idBit & idBits) != 0) { + if (i == oldActionPointerIndex) { + newActionPointerIndex = newPointerCount; + } + map[newPointerCount] = i; + newPointerCount += 1; + newIdBits |= idBit; + } + } + + if (newPointerCount == 0) { + throw new IllegalArgumentException("idBits did not match any ids in the event"); + } + + final int newAction; + if (oldActionMasked == ACTION_POINTER_DOWN || oldActionMasked == ACTION_POINTER_UP) { + if (newActionPointerIndex < 0) { + // An unrelated pointer changed. + newAction = ACTION_MOVE; + } else if (newPointerCount == 1) { + // The first/last pointer went down/up. + newAction = oldActionMasked == ACTION_POINTER_DOWN + ? ACTION_DOWN : ACTION_UP; + } else { + // A secondary pointer went down/up. + newAction = oldActionMasked + | (newActionPointerIndex << ACTION_POINTER_INDEX_SHIFT); + } + } else { + // Simple up/down/cancel/move or other motion action. + newAction = oldAction; + } + + final int historySize = nativeGetHistorySize(mNativePtr); + for (int h = 0; h <= historySize; h++) { + final int historyPos = h == historySize ? HISTORY_CURRENT : h; + + for (int i = 0; i < newPointerCount; i++) { + nativeGetPointerCoords(mNativePtr, map[i], historyPos, pc[i]); + } + + final long eventTimeNanos = nativeGetEventTimeNanos(mNativePtr, historyPos); + if (h == 0) { + ev.mNativePtr = nativeInitialize(ev.mNativePtr, + nativeGetDeviceId(mNativePtr), nativeGetSource(mNativePtr), + newAction, nativeGetFlags(mNativePtr), + nativeGetEdgeFlags(mNativePtr), nativeGetMetaState(mNativePtr), + nativeGetButtonState(mNativePtr), + nativeGetXOffset(mNativePtr), nativeGetYOffset(mNativePtr), + nativeGetXPrecision(mNativePtr), nativeGetYPrecision(mNativePtr), + nativeGetDownTimeNanos(mNativePtr), eventTimeNanos, + newPointerCount, pp, pc); + } else { + nativeAddBatch(ev.mNativePtr, eventTimeNanos, pc, 0); + } + } + return ev; + } + } + @Override public String toString() { - return "MotionEvent{" + Integer.toHexString(System.identityHashCode(this)) - + " pointerId=" + getPointerId(0) - + " action=" + actionToString(getAction()) - + " x=" + getX() - + " y=" + getY() - + " pressure=" + getPressure() - + " size=" + getSize() - + " touchMajor=" + getTouchMajor() - + " touchMinor=" + getTouchMinor() - + " toolMajor=" + getToolMajor() - + " toolMinor=" + getToolMinor() - + " orientation=" + getOrientation() - + " meta=" + KeyEvent.metaStateToString(getMetaState()) - + " pointerCount=" + getPointerCount() - + " historySize=" + getHistorySize() - + " flags=0x" + Integer.toHexString(getFlags()) - + " edgeFlags=0x" + Integer.toHexString(getEdgeFlags()) - + " device=" + getDeviceId() - + " source=0x" + Integer.toHexString(getSource()) - + (getPointerCount() > 1 ? - " pointerId2=" + getPointerId(1) + " x2=" + getX(1) + " y2=" + getY(1) : "") - + "}"; + StringBuilder msg = new StringBuilder(); + msg.append("MotionEvent { action=").append(actionToString(getAction())); + + final int pointerCount = getPointerCount(); + for (int i = 0; i < pointerCount; i++) { + msg.append(", id[").append(i).append("]=").append(getPointerId(i)); + msg.append(", x[").append(i).append("]=").append(getX(i)); + msg.append(", y[").append(i).append("]=").append(getY(i)); + msg.append(", toolType[").append(i).append("]=").append( + toolTypeToString(getToolType(i))); + } + + msg.append(", buttonState=").append(KeyEvent.metaStateToString(getButtonState())); + msg.append(", metaState=").append(KeyEvent.metaStateToString(getMetaState())); + msg.append(", flags=0x").append(Integer.toHexString(getFlags())); + msg.append(", edgeFlags=0x").append(Integer.toHexString(getEdgeFlags())); + msg.append(", pointerCount=").append(pointerCount); + msg.append(", historySize=").append(getHistorySize()); + msg.append(", eventTime=").append(getEventTime()); + msg.append(", downTime=").append(getDownTime()); + msg.append(", deviceId=").append(getDeviceId()); + msg.append(", source=0x").append(Integer.toHexString(getSource())); + msg.append(" }"); + return msg.toString(); } /** @@ -2313,6 +2932,10 @@ public final class MotionEvent extends InputEvent implements Parcelable { return "ACTION_HOVER_MOVE"; case ACTION_SCROLL: return "ACTION_SCROLL"; + case ACTION_HOVER_ENTER: + return "ACTION_HOVER_ENTER"; + case ACTION_HOVER_EXIT: + return "ACTION_HOVER_EXIT"; } int index = (action & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT; switch (action & ACTION_MASK) { @@ -2364,6 +2987,55 @@ public final class MotionEvent extends InputEvent implements Parcelable { } } + /** + * Returns a string that represents the symbolic name of the specified combined + * button state flags such as "0", "BUTTON_PRIMARY", + * "BUTTON_PRIMARY|BUTTON_SECONDARY" or an equivalent numeric constant such as "0x10000000" + * if unknown. + * + * @param buttonState The button state. + * @return The symbolic name of the specified combined button state flags. + * @hide + */ + public static String buttonStateToString(int buttonState) { + if (buttonState == 0) { + return "0"; + } + StringBuilder result = null; + int i = 0; + while (buttonState != 0) { + final boolean isSet = (buttonState & 1) != 0; + buttonState >>>= 1; // unsigned shift! + if (isSet) { + final String name = BUTTON_SYMBOLIC_NAMES[i]; + if (result == null) { + if (buttonState == 0) { + return name; + } + result = new StringBuilder(name); + } else { + result.append('|'); + result.append(name); + } + } + i += 1; + } + return result.toString(); + } + + /** + * Returns a string that represents the symbolic name of the specified tool type + * such as "TOOL_TYPE_FINGER" or an equivalent numeric constant such as "42" if unknown. + * + * @param toolType The tool type. + * @return The symbolic name of the specified tool type. + * @hide + */ + public static String toolTypeToString(int toolType) { + String symbolicName = TOOL_TYPE_SYMBOLIC_NAMES.get(toolType); + return symbolicName != null ? symbolicName : Integer.toString(toolType); + } + public static final Parcelable.Creator<MotionEvent> CREATOR = new Parcelable.Creator<MotionEvent>() { public MotionEvent createFromParcel(Parcel in) { @@ -2391,8 +3063,9 @@ public final class MotionEvent extends InputEvent implements Parcelable { /** * Transfer object for pointer coordinates. * - * Objects of this type can be used to manufacture new {@link MotionEvent} objects - * and to query pointer coordinate information in bulk. + * Objects of this type can be used to specify the pointer coordinates when + * creating new {@link MotionEvent} objects and to query pointer coordinates + * in bulk. * * Refer to {@link InputDevice} for information about how different kinds of * input devices and sources represent pointer coordinates. @@ -2418,6 +3091,15 @@ public final class MotionEvent extends InputEvent implements Parcelable { copyFrom(other); } + /** @hide */ + public static PointerCoords[] createArray(int size) { + PointerCoords[] array = new PointerCoords[size]; + for (int i = 0; i < size; i++) { + array[i] = new PointerCoords(); + } + return array; + } + /** * The X component of the pointer movement. * @@ -2678,4 +3360,71 @@ public final class MotionEvent extends InputEvent implements Parcelable { } } } + + /** + * Transfer object for pointer properties. + * + * Objects of this type can be used to specify the pointer id and tool type + * when creating new {@link MotionEvent} objects and to query pointer properties in bulk. + */ + public static final class PointerProperties { + /** + * Creates a pointer properties object with an invalid pointer id. + */ + public PointerProperties() { + clear(); + } + + /** + * Creates a pointer properties object as a copy of the contents of + * another pointer properties object. + * @param other + */ + public PointerProperties(PointerProperties other) { + copyFrom(other); + } + + /** @hide */ + public static PointerProperties[] createArray(int size) { + PointerProperties[] array = new PointerProperties[size]; + for (int i = 0; i < size; i++) { + array[i] = new PointerProperties(); + } + return array; + } + + /** + * The pointer id. + * Initially set to {@link #INVALID_POINTER_ID} (-1). + * + * @see MotionEvent#getPointerId(int) + */ + public int id; + + /** + * The pointer tool type. + * Initially set to 0. + * + * @see MotionEvent#getToolType(int) + */ + public int toolType; + + /** + * Resets the pointer properties to their initial values. + */ + public void clear() { + id = INVALID_POINTER_ID; + toolType = TOOL_TYPE_UNKNOWN; + } + + /** + * Copies the contents of another pointer properties object. + * + * @param other The pointer properties object to copy. + */ + public void copyFrom(PointerProperties other) { + id = other.id; + toolType = other.toolType; + } + } } diff --git a/core/java/android/view/OrientationEventListener.java b/core/java/android/view/OrientationEventListener.java index 391ba1e..cd48a4f 100755 --- a/core/java/android/view/OrientationEventListener.java +++ b/core/java/android/view/OrientationEventListener.java @@ -21,7 +21,6 @@ import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; -import android.util.Config; import android.util.Log; /** @@ -31,7 +30,7 @@ import android.util.Log; public abstract class OrientationEventListener { private static final String TAG = "OrientationEventListener"; private static final boolean DEBUG = false; - private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean localLOGV = false; private int mOrientation = ORIENTATION_UNKNOWN; private SensorManager mSensorManager; private boolean mEnabled = false; diff --git a/core/java/android/view/ScaleGestureDetector.java b/core/java/android/view/ScaleGestureDetector.java index 6a21b5a..5d2c1a7 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); @@ -465,6 +476,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 3efc799..764899f 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -33,7 +33,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; @@ -83,7 +82,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>(); @@ -427,7 +426,7 @@ public class SurfaceView extends View { if (!mHaveFrame) { return; } - ViewRoot viewRoot = (ViewRoot) getRootView().getParent(); + ViewAncestor viewRoot = (ViewAncestor) getRootView().getParent(); if (viewRoot != null) { mTranslator = viewRoot.mTranslator; } diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java new file mode 100644 index 0000000..755ecf5 --- /dev/null +++ b/core/java/android/view/TextureView.java @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.SurfaceTexture; +import android.util.AttributeSet; +import android.util.Log; + +/** + * <p>A TextureView can be used to display a content stream. Such a content + * stream can for instance be a video or an OpenGL scene. The content stream + * can come from the application's process as well as a remote process.</p> + * + * <p>TextureView can only be used in a hardware accelerated window. When + * rendered in software, TextureView will draw nothing.</p> + * + * <p>Unlike {@link SurfaceView}, TextureView does not create a separate + * window but behaves as a regular View. This key difference allows a + * TextureView to be moved, transformed, animated, etc. For instance, you + * can make a TextureView semi-translucent by calling + * <code>myView.setAlpha(0.5f)</code>.</p> + * + * <p>Using a TextureView is simple: all you need to do is get its + * {@link SurfaceTexture}. The {@link SurfaceTexture} can then be used to + * render content. The following example demonstrates how to render the + * camera preview into a TextureView:</p> + * + * <pre> + * public class LiveCameraActivity extends Activity implements TextureView.SurfaceTextureListener { + * private Camera mCamera; + * private TextureView mTextureView; + * + * protected void onCreate(Bundle savedInstanceState) { + * super.onCreate(savedInstanceState); + * + * mTextureView = new TextureView(this); + * mTextureView.setSurfaceTextureListener(this); + * + * setContentView(mTextureView); + * } + * + * protected void onDestroy() { + * super.onDestroy(); + * + * mCamera.stopPreview(); + * mCamera.release(); + * } + * + * public void onSurfaceTextureAvailable(SurfaceTexture surface) { + * mCamera = Camera.open(); + * + * try { + * mCamera.setPreviewTexture(surface); + * mCamera.startPreview(); + * } catch (IOException ioe) { + * // Something bad happened + * } + * } + * + * public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + * // Ignored, Camera does all the work for us + * } + * } + * </pre> + * + * <p>A TextureView's SurfaceTexture can be obtained either by invoking + * {@link #getSurfaceTexture()} or by using a {@link SurfaceTextureListener}. + * It is important to know that a SurfaceTexture is available only after the + * TextureView is attached to a window (and {@link #onAttachedToWindow()} has + * been invoked.) It is therefore highly recommended you use a listener to + * be notified when the SurfaceTexture becomes available.</p> + * + * @see SurfaceView + * @see SurfaceTexture + */ +public class TextureView extends View { + private HardwareLayer mLayer; + private SurfaceTexture mSurface; + private SurfaceTextureListener mListener; + + private final Runnable mUpdateLayerAction = new Runnable() { + @Override + public void run() { + updateLayer(); + } + }; + private SurfaceTexture.OnFrameAvailableListener mUpdateListener; + + /** + * Creates a new TextureView. + * + * @param context The context to associate this view with. + */ + public TextureView(Context context) { + super(context); + init(); + } + + /** + * Creates a new TextureView. + * + * @param context The context to associate this view with. + * @param attrs The attributes of the XML tag that is inflating the view. + */ + @SuppressWarnings({"UnusedDeclaration"}) + public TextureView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + /** + * Creates a new TextureView. + * + * @param context The context to associate this view with. + * @param attrs The attributes of the XML tag that is inflating the view. + * @param defStyle The default style to apply to this view. If 0, no style + * will be applied (beyond what is included in the theme). This may + * either be an attribute resource, whose value will be retrieved + * from the current theme, or an explicit style resource. + */ + @SuppressWarnings({"UnusedDeclaration"}) + public TextureView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + private void init() { + mLayerPaint = new Paint(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (!isHardwareAccelerated()) { + Log.w("TextureView", "A TextureView or a subclass can only be " + + "used with hardware acceleration enabled."); + } + } + + /** + * The layer type of a TextureView is ignored since a TextureView is always + * considered to act as a hardware layer. The optional paint supplied to this + * method will however be taken into account when rendering the content of + * this TextureView. + * + * @param layerType The ype of layer to use with this view, must be one of + * {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or + * {@link #LAYER_TYPE_HARDWARE} + * @param paint The paint used to compose the layer. This argument is optional + * and can be null. It is ignored when the layer type is + * {@link #LAYER_TYPE_NONE} + */ + @Override + public void setLayerType(int layerType, Paint paint) { + if (paint != mLayerPaint) { + mLayerPaint = paint; + invalidate(); + } + } + + /** + * Always returns {@link #LAYER_TYPE_HARDWARE}. + */ + @Override + public int getLayerType() { + return LAYER_TYPE_HARDWARE; + } + + /** + * Calling this method has no effect. + */ + @Override + public void buildLayer() { + } + + /** + * Subclasses of TextureView cannot do their own rendering + * with the {@link Canvas} object. + * + * @param canvas The Canvas to which the View is rendered. + */ + @Override + public final void draw(Canvas canvas) { + super.draw(canvas); + } + + /** + * Subclasses of TextureView cannot do their own rendering + * with the {@link Canvas} object. + * + * @param canvas The Canvas to which the View is rendered. + */ + @Override + protected final void onDraw(Canvas canvas) { + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (mSurface != null) { + nSetDefaultBufferSize(mSurface.mSurfaceTexture, getWidth(), getHeight()); + } + } + + @Override + HardwareLayer getHardwareLayer() { + if (mAttachInfo == null || mAttachInfo.mHardwareRenderer == null) { + return null; + } + + if (mLayer == null) { + mLayer = mAttachInfo.mHardwareRenderer.createHardwareLayer(); + mSurface = mAttachInfo.mHardwareRenderer.createSuraceTexture(mLayer); + nSetDefaultBufferSize(mSurface.mSurfaceTexture, getWidth(), getHeight()); + + mUpdateListener = new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + // Per SurfaceTexture's documentation, the callback may be invoked + // from an arbitrary thread + post(mUpdateLayerAction); + } + }; + mSurface.setOnFrameAvailableListener(mUpdateListener); + + if (mListener != null) { + mListener.onSurfaceTextureAvailable(mSurface); + } + } + + return mLayer; + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + + if (mSurface != null) { + // When the view becomes invisible, stop updating it, it's a waste of CPU + // To cancel updates, the easiest thing to do is simply to remove the + // updates listener + if (visibility == VISIBLE) { + mSurface.setOnFrameAvailableListener(mUpdateListener); + updateLayer(); + } else { + mSurface.setOnFrameAvailableListener(null); + } + } + } + + private void updateLayer() { + if (mAttachInfo == null || mAttachInfo.mHardwareRenderer == null) { + return; + } + + mAttachInfo.mHardwareRenderer.updateTextureLayer(mLayer, getWidth(), getHeight(), mSurface); + + invalidate(); + } + + /** + * Returns the {@link SurfaceTexture} used by this view. This method + * may return null if the view is not attached to a window. + */ + public SurfaceTexture getSurfaceTexture() { + return mSurface; + } + + /** + * Returns the {@link SurfaceTextureListener} currently associated with this + * texture view. + * + * @see #setSurfaceTextureListener(android.view.TextureView.SurfaceTextureListener) + * @see SurfaceTextureListener + */ + public SurfaceTextureListener getSurfaceTextureListener() { + return mListener; + } + + /** + * Sets the {@link SurfaceTextureListener} used to listen to surface + * texture events. + * + * @see #getSurfaceTextureListener() + * @see SurfaceTextureListener + */ + public void setSurfaceTextureListener(SurfaceTextureListener listener) { + mListener = listener; + } + + /** + * This listener can be used to be notified when the surface texture + * associated with this texture view is available. + */ + public static interface SurfaceTextureListener { + /** + * Invoked when a {@link TextureView}'s SurfaceTexture is ready for use. + * + * @param surface The surface returned by + * {@link android.view.TextureView#getSurfaceTexture()} + */ + public void onSurfaceTextureAvailable(SurfaceTexture surface); + + /** + * Invoked when the {@link SurfaceTexture}'s buffers size changed. + * + * @param surface The surface returned by + * {@link android.view.TextureView#getSurfaceTexture()} + * @param width The new width of the surface + * @param height The new height of the surface + */ + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height); + } + + private static native void nSetDefaultBufferSize(int surfaceTexture, int width, int height); +} diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java index fccef2b..5a91d31 100644 --- a/core/java/android/view/VelocityTracker.java +++ b/core/java/android/view/VelocityTracker.java @@ -50,6 +50,7 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { private int mPtr; private VelocityTracker mNext; + private boolean mIsPooled; private static native int nativeInitialize(); private static native void nativeDispose(int ptr); @@ -93,6 +94,20 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { return mNext; } + /** + * @hide + */ + public boolean isPooled() { + return mIsPooled; + } + + /** + * @hide + */ + public void setPooled(boolean isPooled) { + mIsPooled = isPooled; + } + private VelocityTracker() { mPtr = nativeInitialize(); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index c315884..30ac3f7 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -16,6 +16,8 @@ package android.view; +import android.util.FloatProperty; +import android.util.Property; import com.android.internal.R; import com.android.internal.util.Predicate; import com.android.internal.view.menu.MenuBuilder; @@ -49,7 +51,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.os.SystemClock; -import android.os.SystemProperties; import android.util.AttributeSet; import android.util.Log; import android.util.Pool; @@ -62,6 +63,7 @@ import android.view.ContextMenu.ContextMenuInfo; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEventSource; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; @@ -128,11 +130,11 @@ import java.util.concurrent.CopyOnWriteArrayList; * that will be notified when something interesting happens to the view. For * example, all views will let you set a listener to be notified when the view * gains or loses focus. You can register such a listener using - * {@link #setOnFocusChangeListener}. Other view subclasses offer more - * specialized listeners. For example, a Button exposes a listener to notify - * clients when the button is clicked.</li> + * {@link #setOnFocusChangeListener(android.view.View.OnFocusChangeListener)}. + * Other view subclasses offer more specialized listeners. For example, a Button + * exposes a listener to notify clients when the button is clicked.</li> * <li><strong>Set visibility:</strong> You can hide or show views using - * {@link #setVisibility}.</li> + * {@link #setVisibility(int)}.</li> * </ul> * </p> * <p><em> @@ -173,61 +175,61 @@ import java.util.concurrent.CopyOnWriteArrayList; * * <tr> * <td rowspan="3">Layout</td> - * <td><code>{@link #onMeasure}</code></td> + * <td><code>{@link #onMeasure(int, int)}</code></td> * <td>Called to determine the size requirements for this view and all * of its children. * </td> * </tr> * <tr> - * <td><code>{@link #onLayout}</code></td> + * <td><code>{@link #onLayout(boolean, int, int, int, int)}</code></td> * <td>Called when this view should assign a size and position to all * of its children. * </td> * </tr> * <tr> - * <td><code>{@link #onSizeChanged}</code></td> + * <td><code>{@link #onSizeChanged(int, int, int, int)}</code></td> * <td>Called when the size of this view has changed. * </td> * </tr> * * <tr> * <td>Drawing</td> - * <td><code>{@link #onDraw}</code></td> + * <td><code>{@link #onDraw(android.graphics.Canvas)}</code></td> * <td>Called when the view should render its content. * </td> * </tr> * * <tr> * <td rowspan="4">Event processing</td> - * <td><code>{@link #onKeyDown}</code></td> + * <td><code>{@link #onKeyDown(int, KeyEvent)}</code></td> * <td>Called when a new key event occurs. * </td> * </tr> * <tr> - * <td><code>{@link #onKeyUp}</code></td> + * <td><code>{@link #onKeyUp(int, KeyEvent)}</code></td> * <td>Called when a key up event occurs. * </td> * </tr> * <tr> - * <td><code>{@link #onTrackballEvent}</code></td> + * <td><code>{@link #onTrackballEvent(MotionEvent)}</code></td> * <td>Called when a trackball motion event occurs. * </td> * </tr> * <tr> - * <td><code>{@link #onTouchEvent}</code></td> + * <td><code>{@link #onTouchEvent(MotionEvent)}</code></td> * <td>Called when a touch screen motion event occurs. * </td> * </tr> * * <tr> * <td rowspan="2">Focus</td> - * <td><code>{@link #onFocusChanged}</code></td> + * <td><code>{@link #onFocusChanged(boolean, int, android.graphics.Rect)}</code></td> * <td>Called when the view gains or loses focus. * </td> * </tr> * * <tr> - * <td><code>{@link #onWindowFocusChanged}</code></td> + * <td><code>{@link #onWindowFocusChanged(boolean)}</code></td> * <td>Called when the window containing the view gains or loses focus. * </td> * </tr> @@ -246,7 +248,7 @@ import java.util.concurrent.CopyOnWriteArrayList; * </tr> * * <tr> - * <td><code>{@link #onWindowVisibilityChanged}</code></td> + * <td><code>{@link #onWindowVisibilityChanged(int)}</code></td> * <td>Called when the visibility of the window containing the view * has changed. * </td> @@ -562,15 +564,15 @@ import java.util.concurrent.CopyOnWriteArrayList; * As a remedy, the framework offers a touch filtering mechanism that can be used to * improve the security of views that provide access to sensitive functionality. * </p><p> - * To enable touch filtering, call {@link #setFilterTouchesWhenObscured} or set the + * To enable touch filtering, call {@link #setFilterTouchesWhenObscured(boolean)} or set the * android:filterTouchesWhenObscured layout attribute to true. When enabled, the framework * will discard touches that are received whenever the view's window is obscured by * another visible window. As a result, the view will not receive touches whenever a * toast, dialog or other window appears above the view's window. * </p><p> * For more fine-grained control over security, consider overriding the - * {@link #onFilterTouchEventForSecurity} method to implement your own security policy. - * See also {@link MotionEvent#FLAG_WINDOW_IS_OBSCURED}. + * {@link #onFilterTouchEventForSecurity(MotionEvent)} method to implement your own + * security policy. See also {@link MotionEvent#FLAG_WINDOW_IS_OBSCURED}. * </p> * * @attr ref android.R.styleable#View_alpha @@ -632,7 +634,7 @@ import java.util.concurrent.CopyOnWriteArrayList; * * @see android.view.ViewGroup */ -public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { +public class View implements Drawable.Callback2, KeyEvent.Callback, AccessibilityEventSource { private static final boolean DBG = false; /** @@ -668,19 +670,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility private static final int FITS_SYSTEM_WINDOWS = 0x00000002; /** - * This view is visible. Use with {@link #setVisibility}. + * This view is visible. Use with {@link #setVisibility(int)}. */ public static final int VISIBLE = 0x00000000; /** * This view is invisible, but it still takes up space for layout purposes. - * Use with {@link #setVisibility}. + * Use with {@link #setVisibility(int)}. */ public static final int INVISIBLE = 0x00000004; /** * This view is invisible, and it doesn't take any space for layout - * purposes. Use with {@link #setVisibility}. + * purposes. Use with {@link #setVisibility(int)}. */ public static final int GONE = 0x00000008; @@ -714,10 +716,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility static final int ENABLED_MASK = 0x00000020; /** - * This view won't draw. {@link #onDraw} won't be called and further - * optimizations - * will be performed. It is okay to have this flag set and a background. - * Use with DRAW_MASK when calling setFlags. + * This view won't draw. {@link #onDraw(android.graphics.Canvas)} won't be + * called and further optimizations will be performed. It is okay to have + * this flag set and a background. Use with DRAW_MASK when calling setFlags. * {@hide} */ static final int WILL_NOT_DRAW = 0x00000080; @@ -951,6 +952,54 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility static final int PARENT_SAVE_DISABLED_MASK = 0x20000000; /** + * Horizontal direction of this view is from Left to Right. + * Use with {@link #setLayoutDirection}. + * {@hide} + */ + public static final int LAYOUT_DIRECTION_LTR = 0x00000000; + + /** + * Horizontal direction of this view is from Right to Left. + * Use with {@link #setLayoutDirection}. + * {@hide} + */ + public static final int LAYOUT_DIRECTION_RTL = 0x40000000; + + /** + * Horizontal direction of this view is inherited from its parent. + * Use with {@link #setLayoutDirection}. + * {@hide} + */ + public static final int LAYOUT_DIRECTION_INHERIT = 0x80000000; + + /** + * Horizontal direction of this view is from deduced from the default language + * script for the locale. Use with {@link #setLayoutDirection}. + * {@hide} + */ + public static final int LAYOUT_DIRECTION_LOCALE = 0xC0000000; + + /** + * Mask for use with setFlags indicating bits used for horizontalDirection. + * {@hide} + */ + static final int LAYOUT_DIRECTION_MASK = 0xC0000000; + + /* + * Array of horizontal direction flags for mapping attribute "horizontalDirection" to correct + * flag value. + * {@hide} + */ + private static final int[] LAYOUT_DIRECTION_FLAGS = {LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL, LAYOUT_DIRECTION_INHERIT, LAYOUT_DIRECTION_LOCALE}; + + /** + * Default horizontalDirection. + * {@hide} + */ + private static final int LAYOUT_DIRECTION_DEFAULT = LAYOUT_DIRECTION_INHERIT; + + /** * View flag indicating whether {@link #addFocusables(ArrayList, int, int)} * should add all focusable Views regardless if they are focusable in touch mode. */ @@ -963,34 +1012,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility public static final int FOCUSABLES_TOUCH_MODE = 0x00000001; /** - * Use with {@link #focusSearch}. Move focus to the previous selectable + * Use with {@link #focusSearch(int)}. Move focus to the previous selectable * item. */ public static final int FOCUS_BACKWARD = 0x00000001; /** - * Use with {@link #focusSearch}. Move focus to the next selectable + * Use with {@link #focusSearch(int)}. Move focus to the next selectable * item. */ public static final int FOCUS_FORWARD = 0x00000002; /** - * Use with {@link #focusSearch}. Move focus to the left. + * Use with {@link #focusSearch(int)}. Move focus to the left. */ public static final int FOCUS_LEFT = 0x00000011; /** - * Use with {@link #focusSearch}. Move focus up. + * Use with {@link #focusSearch(int)}. Move focus up. */ public static final int FOCUS_UP = 0x00000021; /** - * Use with {@link #focusSearch}. Move focus to the right. + * Use with {@link #focusSearch(int)}. Move focus to the right. */ public static final int FOCUS_RIGHT = 0x00000042; /** - * Use with {@link #focusSearch}. Move focus down. + * Use with {@link #focusSearch(int)}. Move focus down. */ public static final int FOCUS_DOWN = 0x00000082; @@ -1304,6 +1353,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 +1365,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 { @@ -1440,6 +1495,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility private static final Object sTagsLock = new Object(); /** + * The next available accessiiblity id. + */ + private static int sNextAccessibilityViewId; + + /** * The animation currently associated with this view. * @hide */ @@ -1481,6 +1541,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility int mID = NO_ID; /** + * The stable ID of this view for accessibility porposes. + */ + int mAccessibilityViewId = NO_ID; + + /** * The view's tag. * {@hide} * @@ -1623,6 +1688,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 +1714,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ static final int INVALIDATED = 0x80000000; + /* Masks for mPrivateFlags2 */ + + /** + * Indicates that this view has reported that it can accept the current drag's content. + * Cleared when the drag operation concludes. + * @hide + */ + static final int DRAG_CAN_ACCEPT = 0x00000001; + + /** + * Indicates that this view is currently directly under the drag location in a + * drag-and-drop operation involving content that it can accept. Cleared when + * the drag exits the view, or when the drag operation concludes. + * @hide + */ + static final int DRAG_HOVERED = 0x00000002; + + /** + * Indicates whether the view is drawn in right-to-left direction. + * + * @hide + */ + static final int RESOLVED_LAYOUT_RTL = 0x00000004; + + /* End of masks for mPrivateFlags2 */ + + static final int DRAG_MASK = DRAG_CAN_ACCEPT | DRAG_HOVERED; + /** * Always allow a user to over-scroll this view, provided it is a * view that can scroll. @@ -1814,6 +1913,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 +2343,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 +2450,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 @@ -2364,7 +2466,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility public View(Context context) { mContext = context; mResources = context != null ? context.getResources() : null; - mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED; + mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED | LAYOUT_DIRECTION_INHERIT; mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS); } @@ -2562,6 +2664,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility viewFlagMasks |= VISIBILITY_MASK; } break; + case com.android.internal.R.styleable.View_layoutDirection: + // Clear any HORIZONTAL_DIRECTION flag already set + viewFlagValues &= ~LAYOUT_DIRECTION_MASK; + // Set the HORIZONTAL_DIRECTION flags depending on the value of the attribute + final int layoutDirection = a.getInt(attr, -1); + if (layoutDirection != -1) { + viewFlagValues |= LAYOUT_DIRECTION_FLAGS[layoutDirection]; + } else { + // Set to default (LAYOUT_DIRECTION_INHERIT) + viewFlagValues |= LAYOUT_DIRECTION_DEFAULT; + } + viewFlagMasks |= LAYOUT_DIRECTION_MASK; + break; case com.android.internal.R.styleable.View_drawingCacheQuality: final int cacheQuality = a.getInt(attr, 0); if (cacheQuality != 0) { @@ -2799,8 +2914,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Set the size of the faded edge used to indicate that more content in this * view is available. Will not change whether the fading edge is enabled; use - * {@link #setVerticalFadingEdgeEnabled} or {@link #setHorizontalFadingEdgeEnabled} - * to enable the fading edge for the vertical or horizontal fading edges. + * {@link #setVerticalFadingEdgeEnabled(boolean)} or + * {@link #setHorizontalFadingEdgeEnabled(boolean)} to enable the fading edge + * for the vertical or horizontal fading edges. * * @param length The size in pixels of the faded edge used to indicate that more * content in this view is visible. @@ -3137,6 +3253,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Performs button-related actions during a touch down event. + * + * @param event The event. + * @return True if the down was consumed. + * + * @hide + */ + protected boolean performButtonActionOnTouchDown(MotionEvent event) { + if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) { + if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) { + return true; + } + } + return false; + } + + /** * Bring up the context menu for this view. * * @return Whether a context menu was displayed. @@ -3146,6 +3279,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Bring up the context menu for this view, referring to the item under the specified point. + * + * @param x The referenced x coordinate. + * @param y The referenced y coordinate. + * @param metaState The keyboard modifiers that were pressed. + * @return Whether a context menu was displayed. + * + * @hide + */ + public boolean showContextMenu(float x, float y, int metaState) { + return showContextMenu(); + } + + /** * Start an action mode. * * @param callback Callback that will control the lifecycle of the action mode @@ -3193,7 +3340,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * Give this view focus. This will cause {@link #onFocusChanged} to be called. + * Give this view focus. This will cause + * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called. * * Note: this does not check whether this {@link View} should get focus, it just * gives it focus no matter what. It should only be called internally by framework @@ -3278,7 +3426,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Called when this view wants to give up focus. This will cause - * {@link #onFocusChanged} to be called. + * {@link #onFocusChanged(boolean, int, android.graphics.Rect)} to be called. */ public void clearFocus() { if (DBG) { @@ -3404,7 +3552,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * {@inheritDoc} + * Sends an accessibility event of the given type. If accessiiblity is + * not enabled this method has no effect. The default implementation calls + * {@link #onInitializeAccessibilityEvent(AccessibilityEvent)} first + * to populate information about the event source (this View), then calls + * {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)} to + * populate the text content of the event source including its descendants, + * and last calls + * {@link ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent)} + * on its parent to resuest sending of the event to interested parties. + * + * @param eventType The type of the event to send. + * + * @see #onInitializeAccessibilityEvent(AccessibilityEvent) + * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent) + * @see ViewParent#requestSendAccessibilityEvent(View, AccessibilityEvent) */ public void sendAccessibilityEvent(int eventType) { if (AccessibilityManager.getInstance(mContext).isEnabled()) { @@ -3413,12 +3575,94 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * {@inheritDoc} + * This method behaves exactly as {@link #sendAccessibilityEvent(int)} but + * takes as an argument an empty {@link AccessibilityEvent} and does not + * perfrom a check whether accessibility is enabled. + * + * @param event The event to send. + * + * @see #sendAccessibilityEvent(int) */ public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { if (!isShown()) { return; } + onInitializeAccessibilityEvent(event); + dispatchPopulateAccessibilityEvent(event); + // In the beginning we called #isShown(), so we know that getParent() is not null. + getParent().requestSendAccessibilityEvent(this, event); + } + + /** + * Dispatches an {@link AccessibilityEvent} to the {@link View} first and then + * to its children for adding their text content to the event. Note that the + * event text is populated in a separate dispatch path since we add to the + * event not only the text of the source but also the text of all its descendants. + * </p> + * A typical implementation will call + * {@link #onPopulateAccessibilityEvent(AccessibilityEvent)} on the this view + * and then call the {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)} + * on each child. Override this method if custom population of the event text + * content is required. + * + * @param event The event. + * + * @return True if the event population was completed. + */ + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); + return false; + } + + /** + * Called from {@link #dispatchPopulateAccessibilityEvent(AccessibilityEvent)} + * giving a chance to this View to populate the accessibility event with its + * text content. While the implementation is free to modify other event + * attributes this should be performed in + * {@link #onInitializeAccessibilityEvent(AccessibilityEvent)}. + * <p> + * Example: Adding formatted date string to an accessibility event in addition + * to the text added by the super implementation. + * </p><p><pre><code> + * public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + * super.onPopulateAccessibilityEvent(event); + * final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY; + * String selectedDateUtterance = DateUtils.formatDateTime(mContext, + * mCurrentDate.getTimeInMillis(), flags); + * event.getText().add(selectedDateUtterance); + * } + * </code></pre></p> + * + * @param event The accessibility event which to populate. + * + * @see #sendAccessibilityEvent(int) + * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent) + */ + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + + } + + /** + * Initializes an {@link AccessibilityEvent} with information about the + * the type of the event and this View which is the event source. In other + * words, the source of an accessibility event is the view whose state + * change triggered firing the event. + * <p> + * Example: Setting the password property of an event in addition + * to properties set by the super implementation. + * </p><p><pre><code> + * public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + * super.onInitializeAccessibilityEvent(event); + * event.setPassword(true); + * } + * </code></pre></p> + * @param event The event to initialeze. + * + * @see #sendAccessibilityEvent(int) + * @see #dispatchPopulateAccessibilityEvent(AccessibilityEvent) + */ + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setSource(this); event.setClassName(getClass().getName()); event.setPackageName(getContext().getPackageName()); event.setEnabled(isEnabled()); @@ -3431,22 +3675,112 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility event.setCurrentItemIndex(focusablesTempList.indexOf(this)); focusablesTempList.clear(); } + } - dispatchPopulateAccessibilityEvent(event); + /** + * Returns an {@link AccessibilityNodeInfo} representing this view from the + * point of view of an {@link android.accessibilityservice.AccessibilityService}. + * This method is responsible for obtaining an accessibility node info from a + * pool of reusable instances and calling + * {@link #onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)} on this view to + * initialize the former. + * <p> + * Note: The client is responsible for recycling the obtained instance by calling + * {@link AccessibilityNodeInfo#recycle()} to minimize object creation. + * </p> + * @return A populated {@link AccessibilityNodeInfo}. + * + * @see AccessibilityNodeInfo + */ + public AccessibilityNodeInfo createAccessibilityNodeInfo() { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(this); + onInitializeAccessibilityNodeInfo(info); + return info; + } - AccessibilityManager.getInstance(mContext).sendAccessibilityEvent(event); + /** + * Initializes an {@link AccessibilityNodeInfo} with information about this view. + * The base implementation sets: + * <ul> + * <li>{@link AccessibilityNodeInfo#setParent(View)},</li> + * <li>{@link AccessibilityNodeInfo#setBounds(Rect)},</li> + * <li>{@link AccessibilityNodeInfo#setPackageName(CharSequence)},</li> + * <li>{@link AccessibilityNodeInfo#setClassName(CharSequence)},</li> + * <li>{@link AccessibilityNodeInfo#setContentDescription(CharSequence)},</li> + * <li>{@link AccessibilityNodeInfo#setEnabled(boolean)},</li> + * <li>{@link AccessibilityNodeInfo#setClickable(boolean)},</li> + * <li>{@link AccessibilityNodeInfo#setFocusable(boolean)},</li> + * <li>{@link AccessibilityNodeInfo#setFocused(boolean)},</li> + * <li>{@link AccessibilityNodeInfo#setLongClickable(boolean)},</li> + * <li>{@link AccessibilityNodeInfo#setSelected(boolean)},</li> + * </ul> + * <p> + * Subclasses should override this method, call the super implementation, + * and set additional attributes. + * </p> + * @param info The instance to initialize. + */ + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + Rect bounds = mAttachInfo.mTmpInvalRect; + getDrawingRect(bounds); + info.setBounds(bounds); + + ViewParent parent = getParent(); + if (parent instanceof View) { + View parentView = (View) parent; + info.setParent(parentView); + } + + info.setPackageName(mContext.getPackageName()); + info.setClassName(getClass().getName()); + info.setContentDescription(getContentDescription()); + + info.setEnabled(isEnabled()); + info.setClickable(isClickable()); + info.setFocusable(isFocusable()); + info.setFocused(isFocused()); + info.setSelected(isSelected()); + info.setLongClickable(isLongClickable()); + + // TODO: These make sense only if we are in an AdapterView but all + // views can be selected. Maybe from accessiiblity perspective + // we should report as selectable view in an AdapterView. + info.addAction(AccessibilityNodeInfo.ACTION_SELECT); + info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_SELECTION); + + if (isFocusable()) { + if (isFocused()) { + info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_FOCUS); + } else { + info.addAction(AccessibilityNodeInfo.ACTION_FOCUS); + } + } } /** - * Dispatches an {@link AccessibilityEvent} to the {@link View} children - * to be populated. + * Gets the unique identifier of this view on the screen for accessibility purposes. + * If this {@link View} is not attached to any window, {@value #NO_ID} is returned. * - * @param event The event. + * @return The view accessibility id. * - * @return True if the event population was completed. + * @hide */ - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - return false; + public int getAccessibilityViewId() { + if (mAccessibilityViewId == NO_ID) { + mAccessibilityViewId = sNextAccessibilityViewId++; + } + return mAccessibilityViewId; + } + + /** + * Gets the unique identifier of the window in which this View reseides. + * + * @return The window accessibility id. + * + * @hide + */ + public int getAccessibilityWindowId() { + return mAttachInfo != null ? mAttachInfo.mAccessibilityWindowId : NO_ID; } /** @@ -3924,11 +4258,47 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Returns the layout direction for this view. + * + * @return One of {@link #LAYOUT_DIRECTION_LTR}, + * {@link #LAYOUT_DIRECTION_RTL}, + * {@link #LAYOUT_DIRECTION_INHERIT} or + * {@link #LAYOUT_DIRECTION_LOCALE}. + * @attr ref android.R.styleable#View_layoutDirection + * @hide + */ + @ViewDebug.ExportedProperty(category = "layout", mapping = { + @ViewDebug.IntToString(from = LAYOUT_DIRECTION_LTR, to = "LTR"), + @ViewDebug.IntToString(from = LAYOUT_DIRECTION_RTL, to = "RTL"), + @ViewDebug.IntToString(from = LAYOUT_DIRECTION_INHERIT, to = "INHERIT"), + @ViewDebug.IntToString(from = LAYOUT_DIRECTION_LOCALE, to = "LOCALE") + }) + public int getLayoutDirection() { + return mViewFlags & LAYOUT_DIRECTION_MASK; + } + + /** + * Set the layout direction for this view. + * + * @param layoutDirection One of {@link #LAYOUT_DIRECTION_LTR}, + * {@link #LAYOUT_DIRECTION_RTL}, + * {@link #LAYOUT_DIRECTION_INHERIT} or + * {@link #LAYOUT_DIRECTION_LOCALE}. + * @attr ref android.R.styleable#View_layoutDirection + * @hide + */ + @RemotableViewMethod + public void setLayoutDirection(int layoutDirection) { + setFlags(layoutDirection, LAYOUT_DIRECTION_MASK); + } + + /** * If this view doesn't do any drawing on its own, set this flag to * allow further optimizations. By default, this flag is not set on * View, but could be set on some View subclasses such as ViewGroup. * - * Typically, if you override {@link #onDraw} you should clear this flag. + * Typically, if you override {@link #onDraw(android.graphics.Canvas)} + * you should clear this flag. * * @param willNotDraw whether or not this View draw on its own */ @@ -4057,7 +4427,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * {@link #setPressed(boolean)} is explicitly called, only clickable views can enter * the pressed state. * - * @see #setPressed + * @see #setPressed(boolean) * @see #isClickable() * @see #setClickable(boolean) * @@ -4084,7 +4454,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * Controls whether the saving of this view's state is * enabled (that is, whether its {@link #onSaveInstanceState} method * will be called). Note that even if freezing is enabled, the - * view still must have an id assigned to it (via {@link #setId setId()}) + * view still must have an id assigned to it (via {@link #setId(int)}) * for its state to be saved. This flag can only disable the * saving of this view; any child views may still have their state saved. * @@ -4321,6 +4691,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Finds the Views that contain given text. The containment is case insensitive. + * As View's text is considered any text content that View renders. + * + * @param outViews The output list of matching Views. + * @param text The text to match against. + */ + public void findViewsWithText(ArrayList<View> outViews, CharSequence text) { + } + + /** * Find and return all touchable views that are descendants of this view, * possibly including this view if it is touchable itself. * @@ -4355,7 +4735,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * false), or if it is focusable and it is not focusable in touch mode * ({@link #isFocusableInTouchMode}) while the device is in touch mode. * - * See also {@link #focusSearch}, which is what you call to say that you + * See also {@link #focusSearch(int)}, which is what you call to say that you * have focus, and you want your parent to look for the next one. * * This is equivalent to calling {@link #requestFocus(int, Rect)} with arguments @@ -4376,7 +4756,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * false), or if it is focusable and it is not focusable in touch mode * ({@link #isFocusableInTouchMode}) while the device is in touch mode. * - * See also {@link #focusSearch}, which is what you call to say that you + * See also {@link #focusSearch(int)}, which is what you call to say that you * have focus, and you want your parent to look for the next one. * * This is equivalent to calling {@link #requestFocus(int, Rect)} with @@ -4406,7 +4786,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * {@link android.view.ViewGroup#getDescendantFocusability()} equal to * {@link ViewGroup#FOCUS_BLOCK_DESCENDANTS}. * - * See also {@link #focusSearch}, which is what you call to say that you + * See also {@link #focusSearch(int)}, which is what you call to say that you * have focus, and you want your parent to look for the next one. * * You may wish to override this method if your custom {@link View} has an internal @@ -4427,8 +4807,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility // need to be focusable in touch mode if in touch mode if (isInTouchMode() && - (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) { - return false; + (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) { + return false; } // need to not have any parents blocking us @@ -4440,10 +4820,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility return true; } - /** Gets the ViewRoot, or null if not attached. */ - /*package*/ ViewRoot getViewRoot() { + /** Gets the ViewAncestor, or null if not attached. */ + /*package*/ ViewAncestor getViewAncestor() { View root = getRootView(); - return root != null ? (ViewRoot)root.getParent() : null; + return root != null ? (ViewAncestor)root.getParent() : null; } /** @@ -4459,7 +4839,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility public final boolean requestFocusFromTouch() { // Leave touch mode if we need to if (isInTouchMode()) { - ViewRoot viewRoot = getViewRoot(); + ViewAncestor viewRoot = getViewAncestor(); if (viewRoot != null) { viewRoot.ensureTouchMode(false); } @@ -4516,22 +4896,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * capture information of this view for later analysis: developement only - * check dynamic switch to make sure we only dump view - * when ViewDebug.SYSTEM_PROPERTY_CAPTURE_VIEW) is set - */ - private static void captureViewInfo(String subTag, View v) { - if (v == null || SystemProperties.getInt(ViewDebug.SYSTEM_PROPERTY_CAPTURE_VIEW, 0) == 0) { - return; - } - ViewDebug.dumpCapturedView(subTag, v); - } - - /** * Return the global {@link KeyEvent.DispatcherState KeyEvent.DispatcherState} * for this view's window. Returns null if the view is not currently attached * to the window. Normally you will not need to use this directly, but - * just use the standard high-level event callbacks like {@link #onKeyDown}. + * just use the standard high-level event callbacks like + * {@link #onKeyDown(int, KeyEvent)}. */ public KeyEvent.DispatcherState getKeyDispatcherState() { return mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null; @@ -4562,21 +4931,26 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return True if the event was handled, false otherwise. */ public boolean dispatchKeyEvent(KeyEvent event) { - // If any attached key listener a first crack at the event. - - //noinspection SimplifiableIfStatement,deprecation - if (android.util.Config.LOGV) { - captureViewInfo("captureViewKeyEvent", this); + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onKeyEvent(event, 0); } + // Give any attached key listener a first crack at the event. //noinspection SimplifiableIfStatement if (mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnKeyListener.onKey(this, event.getKeyCode(), event)) { return true; } - return event.dispatch(this, mAttachInfo != null - ? mAttachInfo.mKeyDispatchState : null, this); + if (event.dispatch(this, mAttachInfo != null + ? mAttachInfo.mKeyDispatchState : null, this)) { + return true; + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); + } + return false; } /** @@ -4597,16 +4971,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 +5018,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTrackballEvent(MotionEvent event) { + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTrackballEvent(event, 0); + } + //Log.i("view", "view=" + this + ", " + event.toString()); - return onTrackballEvent(event); + if (onTrackballEvent(event)) { + return true; + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); + } + return false; } /** @@ -4643,28 +5038,101 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * <p> * Generic motion events with source class {@link InputDevice#SOURCE_CLASS_POINTER} * are delivered to the view under the pointer. All other generic motion events are - * delivered to the focused view. + * delivered to the focused view. Hover events are handled specially and are delivered + * to {@link #onHoverEvent(MotionEvent)}. * </p> * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onGenericMotionEvent(event, 0); + } + + final int source = event.getSource(); + if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { + final int action = event.getAction(); + if (action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_MOVE + || action == MotionEvent.ACTION_HOVER_EXIT) { + if (dispatchHoverEvent(event)) { + return true; + } + } else if (dispatchGenericPointerEvent(event)) { + return true; + } + } else if (dispatchGenericFocusedEvent(event)) { + return true; + } + //noinspection SimplifiableIfStatement if (mOnGenericMotionListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnGenericMotionListener.onGenericMotion(this, event)) { return true; } - return onGenericMotionEvent(event); + if (onGenericMotionEvent(event)) { + return true; + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); + } + return false; + } + + /** + * Dispatch a hover event. + * <p> + * Do not call this method directly. + * Call {@link #dispatchGenericMotionEvent(MotionEvent)} instead. + * </p> + * + * @param event The motion event to be dispatched. + * @return True if the event was handled by the view, false otherwise. + * @hide + */ + protected boolean dispatchHoverEvent(MotionEvent event) { + return onHoverEvent(event); + } + + /** + * Dispatch a generic motion event to the view under the first pointer. + * <p> + * Do not call this method directly. + * Call {@link #dispatchGenericMotionEvent(MotionEvent)} instead. + * </p> + * + * @param event The motion event to be dispatched. + * @return True if the event was handled by the view, false otherwise. + * @hide + */ + protected boolean dispatchGenericPointerEvent(MotionEvent event) { + return false; + } + + /** + * Dispatch a generic motion event to the currently focused view. + * <p> + * Do not call this method directly. + * Call {@link #dispatchGenericMotionEvent(MotionEvent)} instead. + * </p> + * + * @param event The motion event to be dispatched. + * @return True if the event was handled by the view, false otherwise. + * @hide + */ + protected boolean dispatchGenericFocusedEvent(MotionEvent event) { + return false; } /** * Dispatch a pointer event. * <p> - * Dispatches touch related pointer events to {@link #onTouchEvent} and all - * other events to {@link #onGenericMotionEvent}. This separation of concerns - * reinforces the invariant that {@link #onTouchEvent} is really about touches + * Dispatches touch related pointer events to {@link #onTouchEvent(MotionEvent)} and all + * other events to {@link #onGenericMotionEvent(MotionEvent)}. This separation of concerns + * reinforces the invariant that {@link #onTouchEvent(MotionEvent)} is really about touches * and should not be expected to handle other pointing device features. * </p> * @@ -4789,7 +5257,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @param visibility The new visibility of the window. * - * @see #onWindowVisibilityChanged + * @see #onWindowVisibilityChanged(int) */ public void dispatchWindowVisibilityChanged(int visibility) { onWindowVisibilityChanged(visibility); @@ -4865,7 +5333,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @param newConfig The new resource configuration. * - * @see #onConfigurationChanged + * @see #onConfigurationChanged(android.content.res.Configuration) */ public void dispatchConfigurationChanged(Configuration newConfig) { onConfigurationChanged(newConfig); @@ -4926,7 +5394,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (mAttachInfo != null) { return mAttachInfo.mInTouchMode; } else { - return ViewRoot.isInTouchMode(); + return ViewAncestor.isInTouchMode(); } } @@ -4981,9 +5449,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) && (event.getRepeatCount() == 0)) { setPressed(true); - if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { - postCheckForLongClick(0); - } + checkForLongClick(0); return true; } break; @@ -5223,15 +5689,80 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * </code> * * @param event The generic motion event being processed. - * - * @return Return true if you have consumed the event, false if you haven't. - * The default implementation always returns false. + * @return True if the event was handled, false otherwise. */ public boolean onGenericMotionEvent(MotionEvent event) { return false; } /** + * Implement this method to handle hover events. + * <p> + * Hover events are pointer events with action {@link MotionEvent#ACTION_HOVER_ENTER}, + * {@link MotionEvent#ACTION_HOVER_MOVE}, or {@link MotionEvent#ACTION_HOVER_EXIT}. + * </p><p> + * The view receives hover enter as the pointer enters the bounds of the view and hover + * exit as the pointer exits the bound of the view or just before the pointer goes down + * (which implies that {@link #onTouchEvent(MotionEvent)} will be called soon). + * </p><p> + * If the view would like to handle the hover event itself and prevent its children + * from receiving hover, it should return true from this method. If this method returns + * true and a child has already received a hover enter event, the child will + * automatically receive a hover exit event. + * </p><p> + * The default implementation sets the hovered state of the view if the view is + * clickable. + * </p> + * + * @param event The motion event that describes the hover. + * @return True if this view handled the hover event and does not want its children + * to receive the hover event. + */ + public boolean onHoverEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + setHovered(true); + break; + + case MotionEvent.ACTION_HOVER_EXIT: + setHovered(false); + break; + } + + return false; + } + + /** + * Returns true if the view is currently hovered. + * + * @return True if the view is currently hovered. + */ + public boolean isHovered() { + return (mPrivateFlags & HOVERED) != 0; + } + + /** + * Sets whether the view is currently hovered. + * + * @param hovered True if the view is hovered. + */ + public void setHovered(boolean hovered) { + if (hovered) { + if ((mPrivateFlags & HOVERED) == 0) { + mPrivateFlags |= HOVERED; + refreshDrawableState(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + } + } else { + if ((mPrivateFlags & HOVERED) != 0) { + mPrivateFlags &= ~HOVERED; + refreshDrawableState(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + } + } + } + + /** * Implement this method to handle touch screen motion events. * * @param event The motion event. @@ -5241,6 +5772,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { + if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) { + mPrivateFlags &= ~PRESSED; + refreshDrawableState(); + } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || @@ -5309,12 +5844,37 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility break; case MotionEvent.ACTION_DOWN: - if (mPendingCheckForTap == null) { - mPendingCheckForTap = new CheckForTap(); - } - mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; - postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + + if (performButtonActionOnTouchDown(event)) { + break; + } + + // Walk up the hierarchy to determine if we're inside a scrolling container. + boolean isInScrollingContainer = false; + ViewParent p = getParent(); + while (p != null && p instanceof ViewGroup) { + if (((ViewGroup) p).shouldDelayChildPressedState()) { + isInScrollingContainer = true; + break; + } + p = p.getParent(); + } + + // For views inside a scrolling container, delay the pressed feedback for + // a short period in case this is a scroll. + if (isInScrollingContainer) { + mPrivateFlags |= PREPRESSED; + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); + } + postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); + } else { + // Not inside a scrolling container, so show the feedback right away + mPrivateFlags |= PRESSED; + refreshDrawableState(); + checkForLongClick(0); + } break; case MotionEvent.ACTION_CANCEL: @@ -5544,6 +6104,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mParent.recomputeViewAttributes(this); } } + + if ((changed & LAYOUT_DIRECTION_MASK) != 0) { + requestLayout(); + } } /** @@ -5631,6 +6195,26 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Set the horizontal scrolled position of your view. This will cause a call to + * {@link #onScrollChanged(int, int, int, int)} and the view will be + * invalidated. + * @param value the x position to scroll to + */ + public void setScrollX(int value) { + scrollTo(value, mScrollY); + } + + /** + * Set the vertical scrolled position of your view. This will cause a call to + * {@link #onScrollChanged(int, int, int, int)} and the view will be + * invalidated. + * @param value the y position to scroll to + */ + public void setScrollY(int value) { + scrollTo(mScrollX, value); + } + + /** * Return the scrolled left position of this view. This is the left edge of * the displayed part of your view. You do not need to draw any pixels * farther left, since those are outside of the frame of your view on @@ -5700,7 +6284,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Return the full width measurement information for this view as computed - * by the most recent call to {@link #measure}. This result is a bit mask + * by the most recent call to {@link #measure(int, int)}. This result is a bit mask * as defined by {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. * This should be used during measurement and layout calculations only. Use * {@link #getWidth()} to see how wide a view is after layout. @@ -5724,7 +6308,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Return the full height measurement information for this view as computed - * by the most recent call to {@link #measure}. This result is a bit mask + * by the most recent call to {@link #measure(int, int)}. This result is a bit mask * as defined by {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. * This should be used during measurement and layout calculations only. Use * {@link #getHeight()} to see how wide a view is after layout. @@ -6693,9 +7277,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * When a view has focus and the user navigates away from it, the next view is searched for * starting from the rectangle filled in by this method. * - * By default, the rectange is the {@link #getDrawingRect})of the view. However, if your - * view maintains some idea of internal selection, such as a cursor, or a selected row - * or column, you should override this method and fill in a more specific rectangle. + * By default, the rectange is the {@link #getDrawingRect(android.graphics.Rect)}) + * of the view. However, if your view maintains some idea of internal selection, + * such as a cursor, or a selected row or column, you should override this method and + * fill in a more specific rectangle. * * @param r The rectangle to fill in, in this view's coordinates. */ @@ -7062,9 +7647,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Mark the the area defined by dirty as needing to be drawn. If the view is - * visible, {@link #onDraw} will be called at some point in the future. - * This must be called from a UI thread. To call from a non-UI thread, call - * {@link #postInvalidate()}. + * visible, {@link #onDraw(android.graphics.Canvas)} will be called at some point + * in the future. This must be called from a UI thread. To call from a non-UI + * thread, call {@link #postInvalidate()}. * * WARNING: This method is destructive to dirty. * @param dirty the rectangle representing the bounds of the dirty region @@ -7085,7 +7670,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { if (p != null && ai != null && ai.mHardwareAccelerated) { // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewRoot to redraw everything + // with a null dirty rect, which tells the ViewAncestor to redraw everything p.invalidateChild(this, null); return; } @@ -7104,9 +7689,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Mark the the area defined by the rect (l,t,r,b) as needing to be drawn. * The coordinates of the dirty rect are relative to the view. - * If the view is visible, {@link #onDraw} will be called at some point - * in the future. This must be called from a UI thread. To call - * from a non-UI thread, call {@link #postInvalidate()}. + * If the view is visible, {@link #onDraw(android.graphics.Canvas)} + * will be called at some point in the future. This must be called from + * a UI thread. To call from a non-UI thread, call {@link #postInvalidate()}. * @param l the left position of the dirty region * @param t the top position of the dirty region * @param r the right position of the dirty region @@ -7128,7 +7713,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { if (p != null && ai != null && ai.mHardwareAccelerated) { // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewRoot to redraw everything + // with a null dirty rect, which tells the ViewAncestor to redraw everything p.invalidateChild(this, null); return; } @@ -7144,9 +7729,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** - * Invalidate the whole view. If the view is visible, {@link #onDraw} will - * be called at some point in the future. This must be called from a - * UI thread. To call from a non-UI thread, call {@link #postInvalidate()}. + * Invalidate the whole view. If the view is visible, + * {@link #onDraw(android.graphics.Canvas)} will be called at some point in + * the future. This must be called from a UI thread. To call from a non-UI thread, + * call {@link #postInvalidate()}. */ public void invalidate() { invalidate(true); @@ -7183,7 +7769,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (!HardwareRenderer.RENDER_DIRTY_REGIONS) { if (p != null && ai != null && ai.mHardwareAccelerated) { // fast-track for GL-enabled applications; just invalidate the whole hierarchy - // with a null dirty rect, which tells the ViewRoot to redraw everything + // with a null dirty rect, which tells the ViewAncestor to redraw everything p.invalidateChild(this, null); return; } @@ -7212,8 +7798,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mPrivateFlags &= ~DRAWN; mPrivateFlags |= INVALIDATED; mPrivateFlags &= ~DRAWING_CACHE_VALID; - if (mParent != null && mAttachInfo != null && mAttachInfo.mHardwareAccelerated) { - mParent.invalidateChild(this, null); + if (mParent != null && mAttachInfo != null) { + if (mAttachInfo.mHardwareAccelerated) { + mParent.invalidateChild(this, null); + } else { + final Rect r = mAttachInfo.mTmpInvalRect; + r.set(0, 0, mRight - mLeft, mBottom - mTop); + // Don't call invalidate -- we don't want to internally scroll + // our own bounds + mParent.invalidateChild(this, r); + } } } } @@ -7319,11 +7913,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ public boolean post(Runnable action) { Handler handler; - if (mAttachInfo != null) { - handler = mAttachInfo.mHandler; + AttachInfo attachInfo = mAttachInfo; + if (attachInfo != null) { + handler = attachInfo.mHandler; } else { // Assume that post will succeed later - ViewRoot.getRunQueue().post(action); + ViewAncestor.getRunQueue().post(action); return true; } @@ -7348,11 +7943,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ public boolean postDelayed(Runnable action, long delayMillis) { Handler handler; - if (mAttachInfo != null) { - handler = mAttachInfo.mHandler; + AttachInfo attachInfo = mAttachInfo; + if (attachInfo != null) { + handler = attachInfo.mHandler; } else { // Assume that post will succeed later - ViewRoot.getRunQueue().postDelayed(action, delayMillis); + ViewAncestor.getRunQueue().postDelayed(action, delayMillis); return true; } @@ -7371,11 +7967,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility */ public boolean removeCallbacks(Runnable action) { Handler handler; - if (mAttachInfo != null) { - handler = mAttachInfo.mHandler; + AttachInfo attachInfo = mAttachInfo; + if (attachInfo != null) { + handler = attachInfo.mHandler; } else { // Assume that post will succeed later - ViewRoot.getRunQueue().removeCallbacks(action); + ViewAncestor.getRunQueue().removeCallbacks(action); return true; } @@ -7419,11 +8016,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 +8041,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 +8053,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility final Message msg = Message.obtain(); msg.what = AttachInfo.INVALIDATE_RECT_MSG; msg.obj = info; - mAttachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds); + attachInfo.mHandler.sendMessageDelayed(msg, delayMilliseconds); } } @@ -8046,9 +8645,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * This is called when the view is attached to a window. At this point it * has a Surface and will start drawing. Note that this function is - * guaranteed to be called before {@link #onDraw}, however it may be called - * any time before the first onDraw -- including before or after - * {@link #onMeasure}. + * guaranteed to be called before {@link #onDraw(android.graphics.Canvas)}, + * however it may be called any time before the first onDraw -- including + * before or after {@link #onMeasure(int, int)}. * * @see #onDetachedFromWindow() */ @@ -8061,6 +8660,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mPrivateFlags &= ~AWAKEN_SCROLL_BARS_ON_ATTACH; } jumpDrawablesToCurrentState(); + resolveLayoutDirection(); + } + + /** + * Resolving the layout direction. LTR is set initially. + * We are supposing here that the parent directionality will be resolved before its children + */ + private void resolveLayoutDirection() { + mPrivateFlags2 &= ~RESOLVED_LAYOUT_RTL; + switch (getLayoutDirection()) { + case LAYOUT_DIRECTION_INHERIT: + // If this is root view, no need to look at parent's layout dir. + if (mParent != null && mParent instanceof ViewGroup && + ((ViewGroup) mParent).isLayoutRtl()) { + mPrivateFlags2 |= RESOLVED_LAYOUT_RTL; + } + break; + case LAYOUT_DIRECTION_RTL: + mPrivateFlags2 |= RESOLVED_LAYOUT_RTL; + break; + } } /** @@ -8221,24 +8841,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @param container The SparseArray in which to save the view's state. * - * @see #restoreHierarchyState - * @see #dispatchSaveInstanceState - * @see #onSaveInstanceState + * @see #restoreHierarchyState(android.util.SparseArray) + * @see #dispatchSaveInstanceState(android.util.SparseArray) + * @see #onSaveInstanceState() */ public void saveHierarchyState(SparseArray<Parcelable> container) { dispatchSaveInstanceState(container); } /** - * Called by {@link #saveHierarchyState} to store the state for this view and its children. - * May be overridden to modify how freezing happens to a view's children; for example, some - * views may want to not store state for their children. + * Called by {@link #saveHierarchyState(android.util.SparseArray)} to store the state for + * this view and its children. May be overridden to modify how freezing happens to a + * view's children; for example, some views may want to not store state for their children. * * @param container The SparseArray in which to save the view's state. * - * @see #dispatchRestoreInstanceState - * @see #saveHierarchyState - * @see #onSaveInstanceState + * @see #dispatchRestoreInstanceState(android.util.SparseArray) + * @see #saveHierarchyState(android.util.SparseArray) + * @see #onSaveInstanceState() */ protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) { @@ -8272,9 +8892,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return Returns a Parcelable object containing the view's current dynamic * state, or null if there is nothing interesting to save. The * default implementation returns null. - * @see #onRestoreInstanceState - * @see #saveHierarchyState - * @see #dispatchSaveInstanceState + * @see #onRestoreInstanceState(android.os.Parcelable) + * @see #saveHierarchyState(android.util.SparseArray) + * @see #dispatchSaveInstanceState(android.util.SparseArray) * @see #setSaveEnabled(boolean) */ protected Parcelable onSaveInstanceState() { @@ -8287,24 +8907,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @param container The SparseArray which holds previously frozen states. * - * @see #saveHierarchyState - * @see #dispatchRestoreInstanceState - * @see #onRestoreInstanceState + * @see #saveHierarchyState(android.util.SparseArray) + * @see #dispatchRestoreInstanceState(android.util.SparseArray) + * @see #onRestoreInstanceState(android.os.Parcelable) */ public void restoreHierarchyState(SparseArray<Parcelable> container) { dispatchRestoreInstanceState(container); } /** - * Called by {@link #restoreHierarchyState} to retrieve the state for this view and its - * children. May be overridden to modify how restoreing happens to a view's children; for - * example, some views may want to not store state for their children. + * Called by {@link #restoreHierarchyState(android.util.SparseArray)} to retrieve the + * state for this view and its children. May be overridden to modify how restoring + * happens to a view's children; for example, some views may want to not store state + * for their children. * * @param container The SparseArray which holds previously saved state. * - * @see #dispatchSaveInstanceState - * @see #restoreHierarchyState - * @see #onRestoreInstanceState + * @see #dispatchSaveInstanceState(android.util.SparseArray) + * @see #restoreHierarchyState(android.util.SparseArray) + * @see #onRestoreInstanceState(android.os.Parcelable) */ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { if (mID != NO_ID) { @@ -8330,9 +8951,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @param state The frozen state that had previously been returned by * {@link #onSaveInstanceState}. * - * @see #onSaveInstanceState - * @see #restoreHierarchyState - * @see #dispatchRestoreInstanceState + * @see #onSaveInstanceState() + * @see #restoreHierarchyState(android.util.SparseArray) + * @see #dispatchRestoreInstanceState(android.util.SparseArray) */ protected void onRestoreInstanceState(Parcelable state) { mPrivateFlags |= SAVE_STATE_CALLED; @@ -8448,6 +9069,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility // Destroy any previous software drawing cache if needed switch (mLayerType) { + case LAYER_TYPE_HARDWARE: + if (mHardwareLayer != null) { + mHardwareLayer.destroy(); + mHardwareLayer = null; + } + // fall through - unaccelerated views may use software layer mechanism instead case LAYER_TYPE_SOFTWARE: if (mDrawingCache != null) { mDrawingCache.recycle(); @@ -8459,12 +9086,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mUnscaledDrawingCache = null; } break; - case LAYER_TYPE_HARDWARE: - if (mHardwareLayer != null) { - mHardwareLayer.destroy(); - mHardwareLayer = null; - } - break; default: break; } @@ -8555,7 +9176,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mLocalDirtyRect.setEmpty(); } - Canvas currentCanvas = mAttachInfo.mHardwareCanvas; + HardwareCanvas currentCanvas = mAttachInfo.mHardwareCanvas; final HardwareCanvas canvas = mHardwareLayer.start(currentCanvas); mAttachInfo.mHardwareCanvas = canvas; try { @@ -9218,9 +9839,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Manually render this view (and all of its children) to the given Canvas. * The view must have already done a full layout before this function is - * called. When implementing a view, implement {@link #onDraw} instead of - * overriding this method. If you do need to override this method, call - * the superclass version. + * called. When implementing a view, implement + * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. + * If you do need to override this method, call the superclass version. * * @param canvas The Canvas to which the View is rendered. */ @@ -9433,8 +10054,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * optimize the drawing of the fading edges. If you do return a non-zero color, the alpha * should be set to 0xFF. * - * @see #setVerticalFadingEdgeEnabled - * @see #setHorizontalFadingEdgeEnabled + * @see #setVerticalFadingEdgeEnabled(boolean) + * @see #setHorizontalFadingEdgeEnabled(boolean) * * @return The known solid color background for this view, or 0 if the color may vary */ @@ -9547,6 +10168,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * <p>Indicates whether or not this view's layout is right-to-left. This is resolved from + * layout attribute and/or the inherited value from the parent.</p> + * + * @return true if the layout is right-to-left. + */ + @ViewDebug.ExportedProperty(category = "layout") + public boolean isLayoutRtl() { + return (mPrivateFlags2 & RESOLVED_LAYOUT_RTL) == RESOLVED_LAYOUT_RTL; + } + + /** * Assign a size and position to a view and all of its * descendants * @@ -9758,6 +10390,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } } + /** + * Check if a given Drawable is in RTL layout direction. + * + * @param who the recipient of the action + */ + public boolean isLayoutRtl(Drawable who) { + return (who == mBGDrawable) && isLayoutRtl(); + } + /** * If your view subclass is displaying its own Drawable objects, it should * override this function and return true for any Drawable it is @@ -9774,8 +10415,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return boolean If true than the Drawable is being displayed in the * view; else false and it is not allowed to animate. * - * @see #unscheduleDrawable - * @see #drawableStateChanged + * @see #unscheduleDrawable(android.graphics.drawable.Drawable) + * @see #drawableStateChanged() */ protected boolean verifyDrawable(Drawable who) { return who == mBGDrawable; @@ -9788,7 +10429,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * <p>Be sure to call through to the superclass when overriding this * function. * - * @see Drawable#setState + * @see Drawable#setState(int[]) */ protected void drawableStateChanged() { Drawable d = mBGDrawable; @@ -9821,9 +10462,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @return The current drawable state * - * @see Drawable#setState - * @see #drawableStateChanged - * @see #onCreateDrawableState + * @see Drawable#setState(int[]) + * @see #drawableStateChanged() + * @see #onCreateDrawableState(int) */ public final int[] getDrawableState() { if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) { @@ -9848,7 +10489,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return Returns an array holding the current {@link Drawable} state of * the view. * - * @see #mergeDrawableStates + * @see #mergeDrawableStates(int[], int[]) */ protected int[] onCreateDrawableState(int extraSpace) { if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE && @@ -9867,12 +10508,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if ((privateFlags & SELECTED) != 0) viewStateIndex |= VIEW_STATE_SELECTED; if (hasWindowFocus()) viewStateIndex |= VIEW_STATE_WINDOW_FOCUSED; if ((privateFlags & ACTIVATED) != 0) viewStateIndex |= VIEW_STATE_ACTIVATED; - if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested) { + if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested && + HardwareRenderer.isAvailable()) { // This is set if HW acceleration is requested, even if the current // process doesn't allow it. This is just to allow app preview // windows to better match their app. viewStateIndex |= VIEW_STATE_ACCELERATED; } + if ((privateFlags & HOVERED) != 0) viewStateIndex |= VIEW_STATE_HOVERED; + + final int privateFlags2 = mPrivateFlags2; + if ((privateFlags2 & DRAG_CAN_ACCEPT) != 0) viewStateIndex |= VIEW_STATE_DRAG_CAN_ACCEPT; + if ((privateFlags2 & DRAG_HOVERED) != 0) viewStateIndex |= VIEW_STATE_DRAG_HOVERED; drawableState = VIEW_STATE_SETS[viewStateIndex]; @@ -9906,10 +10553,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Merge your own state values in <var>additionalState</var> into the base * state values <var>baseState</var> that were returned by - * {@link #onCreateDrawableState}. + * {@link #onCreateDrawableState(int)}. * * @param baseState The base state values returned by - * {@link #onCreateDrawableState}, which will be modified to also hold your + * {@link #onCreateDrawableState(int)}, which will be modified to also hold your * own additional state values. * * @param additionalState The additional state values you would like @@ -9918,7 +10565,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return As a convenience, the <var>baseState</var> array you originally * passed into the function is returned. * - * @see #onCreateDrawableState + * @see #onCreateDrawableState(int) */ protected static int[] mergeDrawableStates(int[] baseState, int[] additionalState) { final int N = baseState.length; @@ -10348,9 +10995,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility viewParent = view.mParent; } - if (viewParent instanceof ViewRoot) { + if (viewParent instanceof ViewAncestor) { // *cough* - final ViewRoot vr = (ViewRoot)viewParent; + final ViewAncestor vr = (ViewAncestor)viewParent; location[1] -= vr.mCurScrollY; } } @@ -10437,8 +11084,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * number. * * @see #NO_ID - * @see #getId - * @see #findViewById + * @see #getId() + * @see #findViewById(int) * * @param id a number used to identify the view * @@ -10477,8 +11124,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @return a positive integer used to identify the view or {@link #NO_ID} * if the view has no ID * - * @see #setId - * @see #findViewById + * @see #setId(int) + * @see #findViewById(int) * @attr ref android.R.styleable#View_id */ @ViewDebug.CapturedViewProperty @@ -11160,7 +11807,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * therefore all View objects remove themselves from the global transparent * region (passed as a parameter to this function). * - * @param region The transparent region for this ViewRoot (window). + * @param region The transparent region for this ViewAncestor (window). * * @return Returns true if the effective visibility of the view at this * point is opaque, regardless of the transparent region; returns false @@ -11347,6 +11994,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @return The View object associate with this builder object. */ + @SuppressWarnings({"JavadocReference"}) final public View getView() { return mView.get(); } @@ -11472,7 +12120,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility surface.unlockCanvasAndPost(canvas); } - final ViewRoot root = getViewRoot(); + final ViewAncestor root = getViewAncestor(); // Cache the local state object for delivery with DragEvents root.setLocalDragState(myLocalState); @@ -11546,6 +12194,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. @@ -11556,7 +12208,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility /** * Given a Drawable whose bounds have been set to draw into this view, - * update a Region being computed for {@link #gatherTransparentRegion} so + * update a Region being computed for + * {@link #gatherTransparentRegion(android.graphics.Region)} so * that any non-transparent parts of the Drawable are removed from the * given transparent region. * @@ -11603,15 +12256,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } } - private void postCheckForLongClick(int delayOffset) { - mHasPerformedLongPress = false; + private void checkForLongClick(int delayOffset) { + if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { + mHasPerformedLongPress = false; - if (mPendingCheckForLongPress == null) { - mPendingCheckForLongPress = new CheckForLongPress(); + if (mPendingCheckForLongPress == null) { + mPendingCheckForLongPress = new CheckForLongPress(); + } + mPendingCheckForLongPress.rememberWindowAttachCount(); + postDelayed(mPendingCheckForLongPress, + ViewConfiguration.getLongPressTimeout() - delayOffset); } - mPendingCheckForLongPress.rememberWindowAttachCount(); - postDelayed(mPendingCheckForLongPress, - ViewConfiguration.getLongPressTimeout() - delayOffset); } /** @@ -11783,6 +12438,169 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility return getVerticalScrollFactor(); } + // + // Properties + // + /** + * A Property wrapper around the <code>alpha</code> functionality handled by the + * {@link View#setAlpha(float)} and {@link View#getAlpha()} methods. + */ + static Property<View, Float> ALPHA = new FloatProperty<View>("alpha") { + @Override + public void setValue(View object, float value) { + object.setAlpha(value); + } + + @Override + public Float get(View object) { + return object.getAlpha(); + } + }; + + /** + * A Property wrapper around the <code>translationX</code> functionality handled by the + * {@link View#setTranslationX(float)} and {@link View#getTranslationX()} methods. + */ + public static Property<View, Float> TRANSLATION_X = new FloatProperty<View>("translationX") { + @Override + public void setValue(View object, float value) { + object.setTranslationX(value); + } + + @Override + public Float get(View object) { + return object.getTranslationX(); + } + }; + + /** + * A Property wrapper around the <code>translationY</code> functionality handled by the + * {@link View#setTranslationY(float)} and {@link View#getTranslationY()} methods. + */ + public static Property<View, Float> TRANSLATION_Y = new FloatProperty<View>("translationY") { + @Override + public void setValue(View object, float value) { + object.setTranslationY(value); + } + + @Override + public Float get(View object) { + return object.getTranslationY(); + } + }; + + /** + * A Property wrapper around the <code>x</code> functionality handled by the + * {@link View#setX(float)} and {@link View#getX()} methods. + */ + public static Property<View, Float> X = new FloatProperty<View>("x") { + @Override + public void setValue(View object, float value) { + object.setX(value); + } + + @Override + public Float get(View object) { + return object.getX(); + } + }; + + /** + * A Property wrapper around the <code>y</code> functionality handled by the + * {@link View#setY(float)} and {@link View#getY()} methods. + */ + public static Property<View, Float> Y = new FloatProperty<View>("y") { + @Override + public void setValue(View object, float value) { + object.setY(value); + } + + @Override + public Float get(View object) { + return object.getY(); + } + }; + + /** + * A Property wrapper around the <code>rotation</code> functionality handled by the + * {@link View#setRotation(float)} and {@link View#getRotation()} methods. + */ + public static Property<View, Float> ROTATION = new FloatProperty<View>("rotation") { + @Override + public void setValue(View object, float value) { + object.setRotation(value); + } + + @Override + public Float get(View object) { + return object.getRotation(); + } + }; + + /** + * A Property wrapper around the <code>rotationX</code> functionality handled by the + * {@link View#setRotationX(float)} and {@link View#getRotationX()} methods. + */ + public static Property<View, Float> ROTATION_X = new FloatProperty<View>("rotationX") { + @Override + public void setValue(View object, float value) { + object.setRotationX(value); + } + + @Override + public Float get(View object) { + return object.getRotationX(); + } + }; + + /** + * A Property wrapper around the <code>rotationY</code> functionality handled by the + * {@link View#setRotationY(float)} and {@link View#getRotationY()} methods. + */ + public static Property<View, Float> ROTATION_Y = new FloatProperty<View>("rotationY") { + @Override + public void setValue(View object, float value) { + object.setRotationY(value); + } + + @Override + public Float get(View object) { + return object.getRotationY(); + } + }; + + /** + * A Property wrapper around the <code>scaleX</code> functionality handled by the + * {@link View#setScaleX(float)} and {@link View#getScaleX()} methods. + */ + public static Property<View, Float> SCALE_X = new FloatProperty<View>("scaleX") { + @Override + public void setValue(View object, float value) { + object.setScaleX(value); + } + + @Override + public Float get(View object) { + return object.getScaleX(); + } + }; + + /** + * A Property wrapper around the <code>scaleY</code> functionality handled by the + * {@link View#setScaleY(float)} and {@link View#getScaleY()} methods. + */ + public static Property<View, Float> SCALE_Y = new FloatProperty<View>("scaleY") { + @Override + public void setValue(View object, float value) { + object.setScaleY(value); + } + + @Override + public Float get(View object) { + return object.getScaleY(); + } + }; + /** * A MeasureSpec encapsulates the layout requirements passed from parent to child. * Each MeasureSpec represents a requirement for either the width or the height. @@ -11923,9 +12741,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mPrivateFlags &= ~PREPRESSED; mPrivateFlags |= PRESSED; refreshDrawableState(); - if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { - postCheckForLongClick(ViewConfiguration.getTapTimeout()); - } + checkForLongClick(ViewConfiguration.getTapTimeout()); } } @@ -12090,12 +12906,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * Interface definition for a callback to be invoked when the status bar changes * visibility. * - * @see #setOnSystemUiVisibilityChangeListener + * @see View#setOnSystemUiVisibilityChangeListener(android.view.View.OnSystemUiVisibilityChangeListener) */ public interface OnSystemUiVisibilityChangeListener { /** * Called when the status bar changes visibility because of a call to - * {@link #setSystemUiVisibility}. + * {@link View#setSystemUiVisibility(int)}. * * @param visibility {@link #STATUS_BAR_VISIBLE} or {@link #STATUS_BAR_HIDDEN}. */ @@ -12196,6 +13012,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility ); private InvalidateInfo mNext; + private boolean mIsPooled; View target; @@ -12219,6 +13036,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility void release() { sPool.release(this); } + + public boolean isPooled() { + return mIsPooled; + } + + public void setPooled(boolean isPooled) { + mIsPooled = isPooled; + } } final IWindowSession mSession; @@ -12229,7 +13054,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility final Callbacks mRootCallbacks; - Canvas mHardwareCanvas; + HardwareCanvas mHardwareCanvas; /** * The top view of the hierarchy. @@ -12254,7 +13079,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility boolean mScalingRequired; /** - * If set, ViewRoot doesn't use its lame animation for when the window resizes. + * If set, ViewAncestor doesn't use its lame animation for when the window resizes. */ boolean mTurnOffWindowResizeAnim; @@ -12333,7 +13158,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility boolean mInTouchMode; /** - * Indicates that ViewRoot should trigger a global layout change + * Indicates that ViewAncestor should trigger a global layout change * the next time it performs a traversal */ boolean mRecomputeGlobalAttributes; @@ -12395,7 +13220,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility Canvas mCanvas; /** - * A Handler supplied by a view's {@link android.view.ViewRoot}. This + * A Handler supplied by a view's {@link android.view.ViewAncestor}. This * handler can be used to pump events in the UI events queue. */ final Handler mHandler; @@ -12429,6 +13254,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility final ArrayList<View> mFocusablesTempList = new ArrayList<View>(24); /** + * The id of the window for accessibility purposes. + */ + int mAccessibilityWindowId = View.NO_ID; + + /** * Creates a new set of attachment information with the specified * events handler and thread. * diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewAncestor.java index 1440a81..17d7454 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewAncestor.java @@ -17,6 +17,7 @@ package android.view; import android.Manifest; +import android.animation.LayoutTransition; import android.app.ActivityManagerNative; import android.content.ClipDescription; import android.content.ComponentCallbacks; @@ -25,7 +26,6 @@ import android.content.pm.PackageManager; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PixelFormat; @@ -45,26 +45,34 @@ import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; -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; +import android.util.Pool; +import android.util.Poolable; +import android.util.PoolableManager; +import android.util.Pools; import android.util.Slog; import android.util.SparseArray; import android.util.TypedValue; import android.view.View.MeasureSpec; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; + import com.android.internal.policy.PolicyManager; +import com.android.internal.util.Predicate; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.IInputMethodCallback; import com.android.internal.view.IInputMethodSession; @@ -74,6 +82,7 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.List; /** * The top of a view hierarchy, implementing the needed protocol between View @@ -83,11 +92,10 @@ import java.util.ArrayList; * {@hide} */ @SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) -public final class ViewRoot extends Handler implements ViewParent, +public final class ViewAncestor extends Handler implements ViewParent, View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { - private static final String TAG = "ViewRoot"; + private static final String TAG = "ViewAncestor"; private static final boolean DBG = false; - private static final boolean SHOW_FPS = false; private static final boolean LOCAL_LOGV = false; /** @noinspection PointlessBooleanExpression*/ private static final boolean DEBUG_DRAW = false || LOCAL_LOGV; @@ -100,6 +108,12 @@ public final class ViewRoot extends Handler implements ViewParent, private static final boolean DEBUG_CONFIGURATION = false || LOCAL_LOGV; private static final boolean WATCH_POINTER = false; + /** + * Set this system property to true to force the view hierarchy to render + * at 60 Hz. This can be used to measure the potential framerate. + */ + private static final String PROPERTY_PROFILE_RENDERING = "viewancestor.profile_rendering"; + private static final boolean MEASURE_LATENCY = false; private static LatencyTimer lt; @@ -121,8 +135,6 @@ public final class ViewRoot extends Handler implements ViewParent, static final ArrayList<ComponentCallbacks> sConfigCallbacks = new ArrayList<ComponentCallbacks>(); - - private static int sDrawTime; long mLastTrackballTime = 0; final TrackballAxis mTrackballAxisX = new TrackballAxis(); @@ -188,6 +200,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; @@ -220,7 +234,7 @@ public final class ViewRoot extends Handler implements ViewParent, final Configuration mLastConfiguration = new Configuration(); final Configuration mPendingConfiguration = new Configuration(); - + class ResizedInfo { Rect coveredInsets; Rect visibleInsets; @@ -233,10 +247,11 @@ public final class ViewRoot extends Handler implements ViewParent, int mScrollY; int mCurScrollY; Scroller mScroller; - Bitmap mResizeBitmap; - long mResizeBitmapStartTime; - int mResizeBitmapDuration; + HardwareLayer mResizeBuffer; + long mResizeBufferStartTime; + int mResizeBufferDuration; static final Interpolator mResizeInterpolator = new AccelerateDecelerateInterpolator(); + private ArrayList<LayoutTransition> mPendingTransitions; final ViewConfiguration mViewConfiguration; @@ -246,14 +261,31 @@ public final class ViewRoot extends Handler implements ViewParent, volatile Object mLocalDragState; final PointF mDragPoint = new PointF(); final PointF mLastTouchPoint = new PointF(); + + private boolean mProfileRendering; + private Thread mRenderProfiler; + private volatile boolean mRenderProfilingEnabled; /** * see {@link #playSoundEffect(int)} */ AudioManager mAudioManager; + final AccessibilityManager mAccessibilityManager; + + AccessibilityInteractionController mAccessibilityInteractionContrtoller; + + AccessibilityInteractionConnectionManager mAccessibilityInteractionConnectionManager; + 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) { @@ -269,7 +301,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } - public ViewRoot(Context context) { + public ViewAncestor(Context context) { super(); if (MEASURE_LATENCY) { @@ -282,7 +314,7 @@ public final class ViewRoot extends Handler implements ViewParent, // done here instead of in the static block because Zygote does not // allow the spawning of threads. getWindowSession(context.getMainLooper()); - + mThread = Thread.currentThread(); mLocation = new WindowLeaked(null); mLocation.fillInStackTrace(); @@ -299,10 +331,17 @@ public final class ViewRoot extends Handler implements ViewParent, mPreviousTransparentRegion = new Region(); mFirst = true; // true for the first time the view is added mAdded = false; + mAccessibilityManager = AccessibilityManager.getInstance(context); + mAccessibilityInteractionConnectionManager = + new AccessibilityInteractionConnectionManager(); + mAccessibilityManager.addAccessibilityStateChangeListener( + mAccessibilityInteractionConnectionManager); mAttachInfo = new View.AttachInfo(sWindowSession, mWindow, this, this); mViewConfiguration = ViewConfiguration.get(context); mDensity = context.getResources().getDisplayMetrics().densityDpi; mFallbackEventHandler = PolicyManager.makeNewFallbackEventHandler(context); + mProfileRendering = Boolean.parseBoolean( + SystemProperties.get(PROPERTY_PROFILE_RENDERING, "false")); } public static void addFirstDrawHandler(Runnable callback) { @@ -487,10 +526,14 @@ public final class ViewRoot extends Handler implements ViewParent, InputQueue.registerInputChannel(mInputChannel, mInputHandler, Looper.myQueue()); } - + view.assignParent(this); mAddedTouchMode = (res&WindowManagerImpl.ADD_FLAG_IN_TOUCH_MODE) != 0; mAppVisible = (res&WindowManagerImpl.ADD_FLAG_APP_VISIBLE) != 0; + + if (mAccessibilityManager.isEnabled()) { + mAccessibilityInteractionConnectionManager.ensureConnection(); + } } } } @@ -503,9 +546,14 @@ public final class ViewRoot extends Handler implements ViewParent, final boolean hardwareAccelerated = (attrs.flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0; - if (attrs != null && hardwareAccelerated) { + if (hardwareAccelerated) { + if (!HardwareRenderer.isAvailable()) { + mAttachInfo.mHardwareAccelerationRequested = true; + return; + } + // Only enable hardware acceleration if we are not in the system process - // The window manager creates ViewRoots to display animated preview windows + // The window manager creates ViewAncestors to display animated preview windows // of launching apps and we don't want those to be hardware accelerated final boolean systemHwAccelerated = @@ -526,8 +574,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; } } } @@ -663,6 +709,15 @@ public final class ViewRoot extends Handler implements ViewParent, public void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; + + //noinspection ConstantConditions + 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); } } @@ -678,10 +733,32 @@ public final class ViewRoot extends Handler implements ViewParent, return mAppVisible ? mView.getVisibility() : View.GONE; } - void disposeResizeBitmap() { - if (mResizeBitmap != null) { - mResizeBitmap.recycle(); - mResizeBitmap = null; + void disposeResizeBuffer() { + if (mResizeBuffer != null) { + mResizeBuffer.destroy(); + mResizeBuffer = null; + } + } + + /** + * Add LayoutTransition to the list of transitions to be started in the next traversal. + * This list will be cleared after the transitions on the list are start()'ed. These + * transitionsa re added by LayoutTransition itself when it sets up animations. The setup + * happens during the layout phase of traversal, which we want to complete before any of the + * animations are started (because those animations may side-effect properties that layout + * depends upon, like the bounding rectangles of the affected views). So we add the transition + * to the list and it is started just prior to starting the drawing phase of traversal. + * + * @param transition The LayoutTransition to be started on the next traversal. + * + * @hide + */ + public void requestTransitionStart(LayoutTransition transition) { + if (mPendingTransitions == null || !mPendingTransitions.contains(transition)) { + if (mPendingTransitions == null) { + mPendingTransitions = new ArrayList<LayoutTransition>(); + } + mPendingTransitions.add(transition); } } @@ -823,15 +900,25 @@ public final class ViewRoot extends Handler implements ViewParent, mAttachInfo.mHardwareRenderer.isEnabled() && lp != null && !PixelFormat.formatHasAlpha(lp.format)) { - disposeResizeBitmap(); + disposeResizeBuffer(); boolean completed = false; + HardwareCanvas canvas = null; try { - mResizeBitmap = Bitmap.createBitmap(mWidth, mHeight, - Bitmap.Config.ARGB_8888); - mResizeBitmap.setHasAlpha(false); - Canvas canvas = new Canvas(mResizeBitmap); + if (mResizeBuffer == null) { + mResizeBuffer = mAttachInfo.mHardwareRenderer.createHardwareLayer( + mWidth, mHeight, false); + } else if (mResizeBuffer.getWidth() != mWidth || + mResizeBuffer.getHeight() != mHeight) { + mResizeBuffer.resize(mWidth, mHeight); + } + canvas = mResizeBuffer.start(mAttachInfo.mHardwareCanvas); + canvas.setViewport(mWidth, mHeight); + canvas.onPreDraw(null); + final int restoreCount = canvas.save(); + canvas.drawColor(0xff000000, PorterDuff.Mode.SRC); + int yoff; final boolean scrolling = mScroller != null && mScroller.computeScrollOffset(); @@ -841,22 +928,32 @@ public final class ViewRoot extends Handler implements ViewParent, } else { yoff = mScrollY; } + canvas.translate(0, -yoff); if (mTranslator != null) { mTranslator.translateCanvas(canvas); } - canvas.setScreenDensity(mAttachInfo.mScalingRequired - ? DisplayMetrics.DENSITY_DEVICE : 0); + mView.draw(canvas); - mResizeBitmapStartTime = SystemClock.uptimeMillis(); - mResizeBitmapDuration = mView.getResources().getInteger( + + mResizeBufferStartTime = SystemClock.uptimeMillis(); + mResizeBufferDuration = mView.getResources().getInteger( com.android.internal.R.integer.config_mediumAnimTime); completed = true; + + canvas.restoreToCount(restoreCount); } catch (OutOfMemoryError e) { Log.w(TAG, "Not enough memory for content change anim buffer", e); } finally { - if (!completed) { - mResizeBitmap = null; + if (canvas != null) { + canvas.onPostDraw(); + } + if (mResizeBuffer != null) { + mResizeBuffer.end(mAttachInfo.mHardwareCanvas); + if (!completed) { + mResizeBuffer.destroy(); + mResizeBuffer = null; + } } } } @@ -1125,7 +1222,7 @@ public final class ViewRoot extends Handler implements ViewParent, if (mScroller != null) { mScroller.abortAnimation(); } - disposeResizeBitmap(); + disposeResizeBuffer(); } else if (surfaceGenerationId != mSurface.getGenerationId() && mSurfaceHolder == null && mAttachInfo.mHardwareRenderer != null) { fullRedrawNeeded = true; @@ -1282,7 +1379,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 " @@ -1407,9 +1504,25 @@ public final class ViewRoot extends Handler implements ViewParent, boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw(); if (!cancelDraw && !newSurface) { + if (mPendingTransitions != null && mPendingTransitions.size() > 0) { + for (int i = 0; i < mPendingTransitions.size(); ++i) { + mPendingTransitions.get(i).startChangingAnimations(); + } + mPendingTransitions.clear(); + } 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) { @@ -1496,15 +1609,62 @@ public final class ViewRoot extends Handler implements ViewParent, int mResizeAlpha; final Paint mResizePaint = new Paint(); - public void onHardwarePreDraw(Canvas canvas) { + public void onHardwarePreDraw(HardwareCanvas canvas) { canvas.translate(0, -mHardwareYOffset); } - public void onHardwarePostDraw(Canvas canvas) { - if (mResizeBitmap != null) { - canvas.translate(0, mHardwareYOffset); + public void onHardwarePostDraw(HardwareCanvas canvas) { + if (mResizeBuffer != null) { mResizePaint.setAlpha(mResizeAlpha); - canvas.drawBitmap(mResizeBitmap, 0, 0, mResizePaint); + canvas.drawHardwareLayer(mResizeBuffer, 0.0f, mHardwareYOffset, mResizePaint); + } + } + + /** + * @hide + */ + void outputDisplayList(View view) { + if (mAttachInfo != null && mAttachInfo.mHardwareCanvas != null) { + DisplayList displayList = view.getDisplayList(); + if (displayList != null) { + mAttachInfo.mHardwareCanvas.outputDisplayList(displayList); + } + } + } + + /** + * @see #PROPERTY_PROFILE_RENDERING + */ + private void profileRendering(boolean enabled) { + if (mProfileRendering) { + mRenderProfilingEnabled = enabled; + if (mRenderProfiler == null) { + mRenderProfiler = new Thread(new Runnable() { + @Override + public void run() { + Log.d(TAG, "Starting profiling thread"); + while (mRenderProfilingEnabled) { + mAttachInfo.mHandler.post(new Runnable() { + @Override + public void run() { + mDirty.set(0, 0, mWidth, mHeight); + scheduleTraversals(); + } + }); + try { + // TODO: This should use vsync when we get an API + Thread.sleep(15); + } catch (InterruptedException e) { + Log.d(TAG, "Exiting profiling thread"); + } + } + } + }, "Rendering Profiler"); + mRenderProfiler.start(); + } else { + mRenderProfiler.interrupt(); + mRenderProfiler = null; + } } } @@ -1523,7 +1683,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } } - + scrollToRectOrFocus(null, false); if (mAttachInfo.mViewScrollChanged) { @@ -1546,15 +1706,15 @@ public final class ViewRoot extends Handler implements ViewParent, boolean scalingRequired = mAttachInfo.mScalingRequired; int resizeAlpha = 0; - if (mResizeBitmap != null) { - long deltaTime = SystemClock.uptimeMillis() - mResizeBitmapStartTime; - if (deltaTime < mResizeBitmapDuration) { - float amt = deltaTime/(float)mResizeBitmapDuration; + if (mResizeBuffer != null) { + long deltaTime = SystemClock.uptimeMillis() - mResizeBufferStartTime; + if (deltaTime < mResizeBufferDuration) { + float amt = deltaTime/(float) mResizeBufferDuration; amt = mResizeInterpolator.getInterpolation(amt); animating = true; resizeAlpha = 255 - (int)(amt*255); } else { - disposeResizeBitmap(); + disposeResizeBuffer(); } } @@ -1566,7 +1726,7 @@ public final class ViewRoot extends Handler implements ViewParent, if (mScroller != null) { mScroller.abortAnimation(); } - disposeResizeBitmap(); + disposeResizeBuffer(); } return; } @@ -1620,8 +1780,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; @@ -1698,18 +1870,10 @@ 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); } - if (SHOW_FPS || ViewDebug.DEBUG_SHOW_FPS) { - int now = (int)SystemClock.elapsedRealtime(); - if (sDrawTime != 0) { - nativeShowFPS(canvas, now - sDrawTime); - } - sDrawTime = now; - } - if (ViewDebug.DEBUG_PROFILE_DRAWING) { EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime); } @@ -1838,7 +2002,7 @@ public final class ViewRoot extends Handler implements ViewParent, if (scrollY != mScrollY) { if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Pan scroll changed: old=" + mScrollY + " , new=" + scrollY); - if (!immediate && mResizeBitmap == null) { + if (!immediate && mResizeBuffer == null) { if (mScroller == null) { mScroller = new Scroller(mView.getContext()); } @@ -1884,20 +2048,22 @@ public final class ViewRoot extends Handler implements ViewParent, public void focusableViewAvailable(View v) { checkThread(); - if (mView != null && !mView.hasFocus()) { - v.requestFocus(); - } else { - // the one case where will transfer focus away from the current one - // is if the current view is a view group that prefers to give focus - // to its children first AND the view is a descendant of it. - mFocusedView = mView.findFocus(); - boolean descendantsHaveDibsOnFocus = - (mFocusedView instanceof ViewGroup) && - (((ViewGroup) mFocusedView).getDescendantFocusability() == - ViewGroup.FOCUS_AFTER_DESCENDANTS); - if (descendantsHaveDibsOnFocus && isViewDescendantOf(v, mFocusedView)) { - // If a view gets the focus, the listener will be invoked from requestChildFocus() + if (mView != null) { + if (!mView.hasFocus()) { v.requestFocus(); + } else { + // the one case where will transfer focus away from the current one + // is if the current view is a view group that prefers to give focus + // to its children first AND the view is a descendant of it. + mFocusedView = mView.findFocus(); + boolean descendantsHaveDibsOnFocus = + (mFocusedView instanceof ViewGroup) && + (((ViewGroup) mFocusedView).getDescendantFocusability() == + ViewGroup.FOCUS_AFTER_DESCENDANTS); + if (descendantsHaveDibsOnFocus && isViewDescendantOf(v, mFocusedView)) { + // If a view gets the focus, the listener will be invoked from requestChildFocus() + v.requestFocus(); + } } } } @@ -1917,6 +2083,10 @@ public final class ViewRoot extends Handler implements ViewParent, mView.dispatchDetachedFromWindow(); } + mAccessibilityInteractionConnectionManager.ensureNoConnection(); + mAccessibilityManager.removeAccessibilityStateChangeListener( + mAccessibilityInteractionConnectionManager); + mView = null; mAttachInfo.mRootView = null; mAttachInfo.mSurface = null; @@ -1933,7 +2103,6 @@ public final class ViewRoot extends Handler implements ViewParent, InputQueue.unregisterInputChannel(mInputChannel); } } - try { sWindowSession.remove(mWindow); } catch (RemoteException e) { @@ -1968,9 +2137,7 @@ public final class ViewRoot extends Handler implements ViewParent, // At this point the resources have been updated to // have the most recent config, whatever that is. Use // the on in them which may be newer. - if (mView != null) { - config = mView.getResources().getConfiguration(); - } + config = mView.getResources().getConfiguration(); if (force || mLastConfiguration.diff(config) != 0) { mLastConfiguration.setTo(config); mView.dispatchConfigurationChanged(config); @@ -2021,6 +2188,10 @@ public final class ViewRoot extends Handler implements ViewParent, public final static int DISPATCH_SYSTEM_UI_VISIBILITY = 1017; public final static int DISPATCH_GENERIC_MOTION = 1018; public final static int UPDATE_CONFIGURATION = 1019; + public final static int DO_PERFORM_ACCESSIBILITY_ACTION = 1020; + public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID = 1021; + public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID = 1022; + public final static int DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT = 1023; @Override public void handleMessage(Message msg) { @@ -2035,11 +2206,27 @@ public final class ViewRoot extends Handler implements ViewParent, break; case DO_TRAVERSAL: if (mProfile) { - Debug.startMethodTracing("ViewRoot"); + Debug.startMethodTracing("ViewAncestor"); + } + + final long traversalStartTime; + if (ViewDebug.DEBUG_LATENCY) { + traversalStartTime = System.nanoTime(); + mLastDrawDurationNanos = 0; } performTraversals(); + if (ViewDebug.DEBUG_LATENCY) { + long now = System.nanoTime(); + Log.d(TAG, "Latency: Spent " + + ((now - traversalStartTime) * 0.000001f) + + "ms in performTraversals(), with " + + (mLastDrawDurationNanos * 0.000001f) + + "ms of that time in draw()"); + mLastTraversalFinishedTimeNanos = now; + } + if (mProfile) { Debug.stopMethodTracing(); mProfile = false; @@ -2102,6 +2289,9 @@ public final class ViewRoot extends Handler implements ViewParent, if (mAdded) { boolean hasWindowFocus = msg.arg1 != 0; mAttachInfo.mHasWindowFocus = hasWindowFocus; + + profileRendering(hasWindowFocus); + if (hasWindowFocus) { boolean inTouchMode = msg.arg2 != 0; ensureTouchModeLocally(inTouchMode); @@ -2174,6 +2364,7 @@ public final class ViewRoot extends Handler implements ViewParent, if ((event.getFlags()&KeyEvent.FLAG_FROM_SYSTEM) != 0) { // The IME is trying to say this event is from the // system! Bad bad bad! + //noinspection UnusedAssignment event = KeyEvent.changeFlags(event, event.getFlags() & ~KeyEvent.FLAG_FROM_SYSTEM); } deliverKeyEventPostIme((KeyEvent)msg.obj, false); @@ -2211,28 +2402,95 @@ public final class ViewRoot extends Handler implements ViewParent, } updateConfiguration(config, false); } break; + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID: { + if (mView != null) { + getAccessibilityInteractionController() + .findAccessibilityNodeInfoByAccessibilityIdUiThread(msg); + } + } break; + case DO_PERFORM_ACCESSIBILITY_ACTION: { + if (mView != null) { + getAccessibilityInteractionController() + .perfromAccessibilityActionUiThread(msg); + } + } break; + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID: { + if (mView != null) { + getAccessibilityInteractionController() + .findAccessibilityNodeInfoByViewIdUiThread(msg); + } + } break; + case DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT: { + if (mView != null) { + getAccessibilityInteractionController() + .findAccessibilityNodeInfosByViewTextUiThread(msg); + } + } break; } } - + private void startInputEvent(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; } /** @@ -2357,6 +2615,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); @@ -2373,9 +2643,6 @@ public final class ViewRoot extends Handler implements ViewParent, if (isDown) { ensureTouchMode(true); } - if(Config.LOGV) { - captureMotionLog("captureDispatchPointer", event); - } // Offset the scroll position. if (mCurScrollY != 0) { @@ -2451,8 +2718,9 @@ 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); } + //noinspection ConstantConditions if (LOCAL_LOGV || WATCH_POINTER) { if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { Log.i(TAG, "Done dispatching!"); @@ -2461,8 +2729,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); @@ -2591,6 +2867,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; @@ -2762,52 +3046,6 @@ public final class ViewRoot extends Handler implements ViewParent, return false; } - /** - * log motion events - */ - private static void captureMotionLog(String subTag, MotionEvent ev) { - //check dynamic switch - if (ev == null || - SystemProperties.getInt(ViewDebug.SYSTEM_PROPERTY_CAPTURE_EVENT, 0) == 0) { - return; - } - - StringBuilder sb = new StringBuilder(subTag + ": "); - sb.append(ev.getDownTime()).append(','); - sb.append(ev.getEventTime()).append(','); - sb.append(ev.getAction()).append(','); - sb.append(ev.getX()).append(','); - sb.append(ev.getY()).append(','); - sb.append(ev.getPressure()).append(','); - sb.append(ev.getSize()).append(','); - sb.append(ev.getMetaState()).append(','); - sb.append(ev.getXPrecision()).append(','); - sb.append(ev.getYPrecision()).append(','); - sb.append(ev.getDeviceId()).append(','); - sb.append(ev.getEdgeFlags()); - Log.d(TAG, sb.toString()); - } - /** - * log motion events - */ - private static void captureKeyLog(String subTag, KeyEvent ev) { - //check dynamic switch - if (ev == null || - SystemProperties.getInt(ViewDebug.SYSTEM_PROPERTY_CAPTURE_EVENT, 0) == 0) { - return; - } - StringBuilder sb = new StringBuilder(subTag + ": "); - sb.append(ev.getDownTime()).append(','); - sb.append(ev.getEventTime()).append(','); - sb.append(ev.getAction()).append(','); - sb.append(ev.getKeyCode()).append(','); - sb.append(ev.getRepeatCount()).append(','); - sb.append(ev.getMetaState()).append(','); - sb.append(ev.getDeviceId()).append(','); - sb.append(ev.getScanCode()); - Log.d(TAG, sb.toString()); - } - int enqueuePendingEvent(Object event, boolean sendDone) { int seq = mPendingEventSeq+1; if (seq < 0) seq = 0; @@ -2826,6 +3064,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); @@ -2872,6 +3118,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); @@ -2884,10 +3134,6 @@ public final class ViewRoot extends Handler implements ViewParent, return; } - if (Config.LOGV) { - captureKeyLog("captureDispatchKeyEvent", event); - } - // Make sure the fallback event policy sees all keys that will be delivered to the // view hierarchy. mFallbackEventHandler.preDispatchKeyEvent(event); @@ -2985,7 +3231,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); } } @@ -3102,6 +3348,17 @@ public final class ViewRoot extends Handler implements ViewParent, return mAudioManager; } + public AccessibilityInteractionController getAccessibilityInteractionController() { + if (mView == null) { + throw new IllegalStateException("getAccessibilityInteractionController" + + " called when there is no mView"); + } + if (mAccessibilityInteractionContrtoller == null) { + mAccessibilityInteractionContrtoller = new AccessibilityInteractionController(); + } + return mAccessibilityInteractionContrtoller; + } + private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) throws RemoteException { @@ -3281,6 +3538,9 @@ 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() { @@ -3322,10 +3582,6 @@ public final class ViewRoot extends Handler implements ViewParent, sendMessageAtTime(msg, event.getEventTime()); } - public void dispatchMotion(MotionEvent event) { - dispatchMotion(event, false); - } - private void dispatchMotion(MotionEvent event, boolean sendDone) { int source = event.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { @@ -3337,10 +3593,6 @@ public final class ViewRoot extends Handler implements ViewParent, } } - public void dispatchPointer(MotionEvent event) { - dispatchPointer(event, false); - } - private void dispatchPointer(MotionEvent event, boolean sendDone) { Message msg = obtainMessage(DISPATCH_POINTER); msg.obj = event; @@ -3348,10 +3600,6 @@ public final class ViewRoot extends Handler implements ViewParent, sendMessageAtTime(msg, event.getEventTime()); } - public void dispatchTrackball(MotionEvent event) { - dispatchTrackball(event, false); - } - private void dispatchTrackball(MotionEvent event, boolean sendDone) { Message msg = obtainMessage(DISPATCH_TRACKBALL); msg.obj = event; @@ -3413,7 +3661,7 @@ public final class ViewRoot extends Handler implements ViewParent, * send an {@link AccessibilityEvent} to announce that. */ private void sendAccessibilityEvents() { - if (!AccessibilityManager.getInstance(mView.getContext()).isEnabled()) { + if (!mAccessibilityManager.isEnabled()) { return; } mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); @@ -3437,6 +3685,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; + } + mAccessibilityManager.sendAccessibilityEvent(event); + return true; + } + void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( @@ -3445,7 +3701,7 @@ public final class ViewRoot extends Handler implements ViewParent, } public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { - // ViewRoot never intercepts touch event, so this can be a no-op + // ViewAncestor never intercepts touch event, so this can be a no-op } public boolean requestChildRectangleOnScreen(View child, Rect rectangle, @@ -3494,57 +3750,58 @@ public final class ViewRoot extends Handler implements ViewParent, } static class InputMethodCallback extends IInputMethodCallback.Stub { - private WeakReference<ViewRoot> mViewRoot; + private WeakReference<ViewAncestor> mViewAncestor; - public InputMethodCallback(ViewRoot viewRoot) { - mViewRoot = new WeakReference<ViewRoot>(viewRoot); + public InputMethodCallback(ViewAncestor viewAncestor) { + mViewAncestor = new WeakReference<ViewAncestor>(viewAncestor); } public void finishedEvent(int seq, boolean handled) { - final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { - viewRoot.dispatchFinishedEvent(seq, handled); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchFinishedEvent(seq, handled); } } - public void sessionCreated(IInputMethodSession session) throws RemoteException { + public void sessionCreated(IInputMethodSession session) { // Stub -- not for use in the client. } } static class W extends IWindow.Stub { - private final WeakReference<ViewRoot> mViewRoot; + private final WeakReference<ViewAncestor> mViewAncestor; - W(ViewRoot viewRoot) { - mViewRoot = new WeakReference<ViewRoot>(viewRoot); + W(ViewAncestor viewAncestor) { + mViewAncestor = new WeakReference<ViewAncestor>(viewAncestor); } public void resized(int w, int h, Rect coveredInsets, Rect visibleInsets, boolean reportDraw, Configuration newConfig) { - final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { - viewRoot.dispatchResized(w, h, coveredInsets, visibleInsets, reportDraw, newConfig); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchResized(w, h, coveredInsets, visibleInsets, reportDraw, + newConfig); } } public void dispatchAppVisibility(boolean visible) { - final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { - viewRoot.dispatchAppVisibility(visible); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchAppVisibility(visible); } } public void dispatchGetNewSurface() { - final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { - viewRoot.dispatchGetNewSurface(); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchGetNewSurface(); } } public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) { - final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { - viewRoot.windowFocusChanged(hasFocus, inTouchMode); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.windowFocusChanged(hasFocus, inTouchMode); } } @@ -3562,9 +3819,9 @@ public final class ViewRoot extends Handler implements ViewParent, } public void executeCommand(String command, String parameters, ParcelFileDescriptor out) { - final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { - final View view = viewRoot.mView; + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + final View view = viewAncestor.mView; if (view != null) { if (checkCallingPermission(Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { @@ -3593,9 +3850,9 @@ public final class ViewRoot extends Handler implements ViewParent, } public void closeSystemDialogs(String reason) { - final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { - viewRoot.dispatchCloseSystemDialogs(reason); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchCloseSystemDialogs(reason); } } @@ -3608,7 +3865,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } } - + public void dispatchWallpaperCommand(String action, int x, int y, int z, Bundle extras, boolean sync) { if (sync) { @@ -3621,17 +3878,16 @@ public final class ViewRoot extends Handler implements ViewParent, /* Drag/drop */ public void dispatchDragEvent(DragEvent event) { - final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { - viewRoot.dispatchDragEvent(event); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchDragEvent(event); } } - @Override public void dispatchSystemUiVisibilityChanged(int visibility) { - final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { - viewRoot.dispatchSystemUiVisibilityChanged(visibility); + final ViewAncestor viewAncestor = mViewAncestor.get(); + if (viewAncestor != null) { + viewAncestor.dispatchSystemUiVisibilityChanged(visibility); } } } @@ -3941,5 +4197,384 @@ public final class ViewRoot extends Handler implements ViewParent, } } - private static native void nativeShowFPS(Canvas canvas, int durationMillis); + /** + * Class for managing the accessibility interaction connection + * based on the global accessibility state. + */ + final class AccessibilityInteractionConnectionManager + implements AccessibilityStateChangeListener { + public void onAccessibilityStateChanged(boolean enabled) { + if (enabled) { + ensureConnection(); + } else { + ensureNoConnection(); + } + } + + public void ensureConnection() { + final boolean registered = mAttachInfo.mAccessibilityWindowId != View.NO_ID; + if (!registered) { + mAttachInfo.mAccessibilityWindowId = + mAccessibilityManager.addAccessibilityInteractionConnection(mWindow, + new AccessibilityInteractionConnection(ViewAncestor.this)); + } + } + + public void ensureNoConnection() { + final boolean registered = mAttachInfo.mAccessibilityWindowId != View.NO_ID; + if (registered) { + mAttachInfo.mAccessibilityWindowId = View.NO_ID; + mAccessibilityManager.removeAccessibilityInteractionConnection(mWindow); + } + } + } + + /** + * This class is an interface this ViewAncestor provides to the + * AccessibilityManagerService to the latter can interact with + * the view hierarchy in this ViewAncestor. + */ + final class AccessibilityInteractionConnection + extends IAccessibilityInteractionConnection.Stub { + private final WeakReference<ViewAncestor> mViewAncestor; + + AccessibilityInteractionConnection(ViewAncestor viewAncestor) { + mViewAncestor = new WeakReference<ViewAncestor>(viewAncestor); + } + + public void findAccessibilityNodeInfoByAccessibilityId(int accessibilityId, + int interactionId, IAccessibilityInteractionConnectionCallback callback) { + if (mViewAncestor.get() != null) { + getAccessibilityInteractionController() + .findAccessibilityNodeInfoByAccessibilityIdClientThread(accessibilityId, + interactionId, callback); + } + } + + public void performAccessibilityAction(int accessibilityId, int action, + int interactionId, IAccessibilityInteractionConnectionCallback callback) { + if (mViewAncestor.get() != null) { + getAccessibilityInteractionController() + .performAccessibilityActionClientThread(accessibilityId, action, interactionId, + callback); + } + } + + public void findAccessibilityNodeInfoByViewId(int viewId, + int interactionId, IAccessibilityInteractionConnectionCallback callback) { + if (mViewAncestor.get() != null) { + getAccessibilityInteractionController() + .findAccessibilityNodeInfoByViewIdClientThread(viewId, interactionId, callback); + } + } + + public void findAccessibilityNodeInfosByViewText(String text, int interactionId, + IAccessibilityInteractionConnectionCallback callback) { + if (mViewAncestor.get() != null) { + getAccessibilityInteractionController() + .findAccessibilityNodeInfosByViewTextClientThread(text, interactionId, + callback); + } + } + } + + /** + * Class for managing accessibility interactions initiated from the system + * and targeting the view hierarchy. A *ClientThread method is to be + * called from the interaction connection this ViewAncestor gives the + * system to talk to it and a corresponding *UiThread method that is executed + * on the UI thread. + */ + final class AccessibilityInteractionController { + private static final int POOL_SIZE = 5; + + private FindByAccessibilitytIdPredicate mFindByAccessibilityIdPredicate = + new FindByAccessibilitytIdPredicate(); + + private ArrayList<AccessibilityNodeInfo> mTempAccessibilityNodeInfoList = + new ArrayList<AccessibilityNodeInfo>(); + + // Reusable poolable arguments for interacting with the view hierarchy + // to fit more arguments than Message and to avoid sharing objects between + // two messages since several threads can send messages concurrently. + private final Pool<SomeArgs> mPool = Pools.synchronizedPool(Pools.finitePool( + new PoolableManager<SomeArgs>() { + public SomeArgs newInstance() { + return new SomeArgs(); + } + + public void onAcquired(SomeArgs info) { + /* do nothing */ + } + + public void onReleased(SomeArgs info) { + info.clear(); + } + }, POOL_SIZE) + ); + + public class SomeArgs implements Poolable<SomeArgs> { + private SomeArgs mNext; + private boolean mIsPooled; + + public Object arg1; + public Object arg2; + public int argi1; + public int argi2; + public int argi3; + + public SomeArgs getNextPoolable() { + return mNext; + } + + public boolean isPooled() { + return mIsPooled; + } + + public void setNextPoolable(SomeArgs args) { + mNext = args; + } + + public void setPooled(boolean isPooled) { + mIsPooled = isPooled; + } + + private void clear() { + arg1 = null; + arg2 = null; + argi1 = 0; + argi2 = 0; + argi3 = 0; + } + } + + public void findAccessibilityNodeInfoByAccessibilityIdClientThread(int accessibilityId, + int interactionId, IAccessibilityInteractionConnectionCallback callback) { + Message message = Message.obtain(); + message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_ACCESSIBILITY_ID; + message.arg1 = accessibilityId; + message.arg2 = interactionId; + message.obj = callback; + sendMessage(message); + } + + public void findAccessibilityNodeInfoByAccessibilityIdUiThread(Message message) { + final int accessibilityId = message.arg1; + final int interactionId = message.arg2; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) message.obj; + + AccessibilityNodeInfo info = null; + try { + FindByAccessibilitytIdPredicate predicate = mFindByAccessibilityIdPredicate; + predicate.init(accessibilityId); + View root = ViewAncestor.this.mView; + View target = root.findViewByPredicate(predicate); + if (target != null) { + info = target.createAccessibilityNodeInfo(); + } + } finally { + try { + callback.setFindAccessibilityNodeInfoResult(info, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void findAccessibilityNodeInfoByViewIdClientThread(int viewId, int interactionId, + IAccessibilityInteractionConnectionCallback callback) { + Message message = Message.obtain(); + message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_ID; + message.arg1 = viewId; + message.arg2 = interactionId; + message.obj = callback; + sendMessage(message); + } + + public void findAccessibilityNodeInfoByViewIdUiThread(Message message) { + final int viewId = message.arg1; + final int interactionId = message.arg2; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) message.obj; + + AccessibilityNodeInfo info = null; + try { + View root = ViewAncestor.this.mView; + View target = root.findViewById(viewId); + if (target != null) { + info = target.createAccessibilityNodeInfo(); + } + } finally { + try { + callback.setFindAccessibilityNodeInfoResult(info, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void findAccessibilityNodeInfosByViewTextClientThread(String text, int interactionId, + IAccessibilityInteractionConnectionCallback callback) { + Message message = Message.obtain(); + message.what = DO_FIND_ACCESSIBLITY_NODE_INFO_BY_VIEW_TEXT; + SomeArgs args = mPool.acquire(); + args.arg1 = text; + args.argi1 = interactionId; + args.arg2 = callback; + message.obj = args; + sendMessage(message); + } + + public void findAccessibilityNodeInfosByViewTextUiThread(Message message) { + SomeArgs args = (SomeArgs) message.obj; + final String text = (String) args.arg1; + final int interactionId = args.argi1; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg2; + mPool.release(args); + + List<AccessibilityNodeInfo> infos = null; + try { + View root = ViewAncestor.this.mView; + + ArrayList<View> foundViews = mAttachInfo.mFocusablesTempList; + foundViews.clear(); + + root.findViewsWithText(foundViews, text); + if (foundViews.isEmpty()) { + return; + } + + infos = mTempAccessibilityNodeInfoList; + infos.clear(); + + final int viewCount = foundViews.size(); + for (int i = 0; i < viewCount; i++) { + View foundView = foundViews.get(i); + infos.add(foundView.createAccessibilityNodeInfo()); + } + } finally { + try { + callback.setFindAccessibilityNodeInfosResult(infos, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + public void performAccessibilityActionClientThread(int accessibilityId, int action, + int interactionId, IAccessibilityInteractionConnectionCallback callback) { + Message message = Message.obtain(); + message.what = DO_PERFORM_ACCESSIBILITY_ACTION; + SomeArgs args = mPool.acquire(); + args.argi1 = accessibilityId; + args.argi2 = action; + args.argi3 = interactionId; + args.arg1 = callback; + message.obj = args; + sendMessage(message); + } + + public void perfromAccessibilityActionUiThread(Message message) { + SomeArgs args = (SomeArgs) message.obj; + final int accessibilityId = args.argi1; + final int action = args.argi2; + final int interactionId = args.argi3; + final IAccessibilityInteractionConnectionCallback callback = + (IAccessibilityInteractionConnectionCallback) args.arg1; + mPool.release(args); + + boolean succeeded = false; + try { + switch (action) { + case AccessibilityNodeInfo.ACTION_FOCUS: { + succeeded = performActionFocus(accessibilityId); + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { + succeeded = performActionClearFocus(accessibilityId); + } break; + case AccessibilityNodeInfo.ACTION_SELECT: { + succeeded = performActionSelect(accessibilityId); + } break; + case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { + succeeded = performActionClearSelection(accessibilityId); + } break; + } + } finally { + try { + callback.setPerformAccessibilityActionResult(succeeded, interactionId); + } catch (RemoteException re) { + /* ignore - the other side will time out */ + } + } + } + + private boolean performActionFocus(int accessibilityId) { + View target = findViewByAccessibilityId(accessibilityId); + if (target == null) { + return false; + } + // Get out of touch mode since accessibility wants to move focus around. + ensureTouchMode(false); + return target.requestFocus(); + } + + private boolean performActionClearFocus(int accessibilityId) { + View target = findViewByAccessibilityId(accessibilityId); + if (target == null) { + return false; + } + if (!target.isFocused()) { + return false; + } + target.clearFocus(); + return !target.isFocused(); + } + + private boolean performActionSelect(int accessibilityId) { + View target = findViewByAccessibilityId(accessibilityId); + if (target == null) { + return false; + } + if (target.isSelected()) { + return false; + } + target.setSelected(true); + return target.isSelected(); + } + + private boolean performActionClearSelection(int accessibilityId) { + View target = findViewByAccessibilityId(accessibilityId); + if (target == null) { + return false; + } + if (!target.isSelected()) { + return false; + } + target.setSelected(false); + return !target.isSelected(); + } + + private View findViewByAccessibilityId(int accessibilityId) { + View root = ViewAncestor.this.mView; + if (root == null) { + return null; + } + mFindByAccessibilityIdPredicate.init(accessibilityId); + return root.findViewByPredicate(mFindByAccessibilityIdPredicate); + } + + private final class FindByAccessibilitytIdPredicate implements Predicate<View> { + public int mSerchedId; + + public void init(int searchedId) { + mSerchedId = searchedId; + } + + public boolean apply(View view) { + return (view.getAccessibilityViewId() == mSerchedId); + } + } + } } diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index df8f7d6..5919150 100755..100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -19,7 +19,6 @@ package android.view; import android.app.AppGlobals; import android.content.Context; import android.content.res.Configuration; -import android.os.Bundle; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.SparseArray; @@ -96,7 +95,7 @@ public class ViewConfiguration { * is a tap or a scroll. If the user does not move within this interval, it is * considered to be a tap. */ - private static final int TAP_TIMEOUT = 115; + private static final int TAP_TIMEOUT = 180; /** * Defines the duration in milliseconds we will wait to see if a touch event @@ -143,12 +142,6 @@ public class ViewConfiguration { private static final int TOUCH_SLOP = 16; /** - * Distance a touch can wander before we think the user is the first touch - * in a sequence of double tap - */ - private static final int LARGE_TOUCH_SLOP = 18; - - /** * Distance a touch can wander before we think the user is attempting a paged scroll * (in dips) */ @@ -176,6 +169,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. */ @@ -203,9 +203,9 @@ public class ViewConfiguration { private final int mMaximumFlingVelocity; private final int mScrollbarSize; private final int mTouchSlop; - private final int mLargeTouchSlop; private final int mPagingTouchSlop; private final int mDoubleTapSlop; + private final int mScaledTouchExplorationTapSlop; private final int mWindowTouchSlop; private final int mMaximumDrawingCacheSize; private final int mOverscrollDistance; @@ -225,9 +225,9 @@ public class ViewConfiguration { mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY; mScrollbarSize = SCROLL_BAR_SIZE; mTouchSlop = TOUCH_SLOP; - mLargeTouchSlop = LARGE_TOUCH_SLOP; mPagingTouchSlop = PAGING_TOUCH_SLOP; mDoubleTapSlop = DOUBLE_TAP_SLOP; + mScaledTouchExplorationTapSlop = TOUCH_EXPLORATION_TAP_SLOP; mWindowTouchSlop = WINDOW_TOUCH_SLOP; //noinspection deprecation mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE; @@ -262,9 +262,9 @@ public class ViewConfiguration { mMaximumFlingVelocity = (int) (density * MAXIMUM_FLING_VELOCITY + 0.5f); mScrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f); mTouchSlop = (int) (sizeAndDensity * TOUCH_SLOP + 0.5f); - mLargeTouchSlop = (int) (sizeAndDensity * LARGE_TOUCH_SLOP + 0.5f); mPagingTouchSlop = (int) (sizeAndDensity * PAGING_TOUCH_SLOP + 0.5f); mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f); + mScaledTouchExplorationTapSlop = (int) (density * TOUCH_EXPLORATION_TAP_SLOP + 0.5f); mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f); // Size of the screen in bytes, in ARGB_8888 format @@ -459,14 +459,6 @@ public class ViewConfiguration { } /** - * @return Distance a touch can wander before we think the user is the first touch - * in a sequence of double tap - */ - public int getScaledLargeTouchSlop() { - return mLargeTouchSlop; - } - - /** * @return Distance a touch can wander before we think the user is scrolling a full page * in dips */ @@ -495,6 +487,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..f014070 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -16,7 +16,6 @@ package android.view; -import android.util.Config; import android.util.Log; import android.util.DisplayMetrics; import android.content.res.Resources; @@ -93,27 +92,6 @@ public class ViewDebug { public static final boolean TRACE_RECYCLER = false; /** - * Enables or disables motion events tracing. Any invoker of - * {@link #trace(View, MotionEvent, MotionEventTraceType)} should first check - * that this value is set to true as not to affect performance. - * - * @hide - */ - public static final boolean TRACE_MOTION_EVENTS = false; - - /** - * The system property of dynamic switch for capturing view information - * when it is set, we dump interested fields and methods for the view on focus - */ - static final String SYSTEM_PROPERTY_CAPTURE_VIEW = "debug.captureview"; - - /** - * The system property of dynamic switch for capturing event information - * when it is set, we log key events, touch/motion and trackball events - */ - static final String SYSTEM_PROPERTY_CAPTURE_EVENT = "debug.captureevent"; - - /** * Profiles drawing times in the events log. * * @hide @@ -141,8 +119,24 @@ public class ViewDebug { public static final boolean DEBUG_DRAG = false; /** + * Enables logging of factors that affect the latency and responsiveness of an application. + * + * Logs the relative difference between the time an event was created and the time it + * was delivered. + * + * Logs the time spent waiting for Surface.lockCanvas() or eglSwapBuffers(). + * This is time that the event loop spends blocked and unresponsive. Ideally, drawing + * and animations should be perfectly synchronized with VSYNC so that swap buffers + * is instantaneous. + * + * Logs the time spent in ViewRoot.performTraversals() or ViewRoot.draw(). + * @hide + */ + public static final boolean DEBUG_LATENCY = false; + + /** * <p>Enables or disables views consistency check. Even when this property is enabled, - * view consistency checks happen only if {@link android.util.Config#DEBUG} is set + * view consistency checks happen only if {@link false} is set * to true. The value of this property can be configured externally in one of the * following files:</p> * <ul> @@ -155,12 +149,6 @@ public class ViewDebug { @Debug.DebugProperty public static boolean consistencyCheckEnabled = false; - static { - if (Config.DEBUG) { - Debug.setFieldsOn(ViewDebug.class, true); - } - } - /** * This annotation can be used to mark fields and methods to be dumped by * the view server. Only non-void methods with no arguments can be annotated @@ -360,6 +348,7 @@ public class ViewDebug { private static final String REMOTE_COMMAND_REQUEST_LAYOUT = "REQUEST_LAYOUT"; private static final String REMOTE_PROFILE = "PROFILE"; private static final String REMOTE_COMMAND_CAPTURE_LAYERS = "CAPTURE_LAYERS"; + private static final String REMOTE_COMMAND_OUTPUT_DISPLAYLIST = "OUTPUT_DISPLAYLIST"; private static HashMap<Class<?>, Field[]> sFieldsForClasses; private static HashMap<Class<?>, Method[]> sMethodsForClasses; @@ -380,7 +369,7 @@ public class ViewDebug { } private static BufferedWriter sHierarchyTraces; - private static ViewRoot sHierarhcyRoot; + private static ViewAncestor sHierarhcyRoot; private static String sHierarchyTracePrefix; /** @@ -408,21 +397,6 @@ public class ViewDebug { private static String sRecyclerTracePrefix; /** - * Defines the type of motion events trace to output to the motion events traces file. - * - * @hide - */ - public enum MotionEventTraceType { - DISPATCH, - ON_INTERCEPT, - ON_TOUCH - } - - private static BufferedWriter sMotionEventTraces; - private static ViewRoot sMotionEventRoot; - private static String sMotionEventTracePrefix; - - /** * Returns the number of instanciated Views. * * @return The number of Views instanciated in the current process. @@ -434,14 +408,14 @@ public class ViewDebug { } /** - * Returns the number of instanciated ViewRoots. + * Returns the number of instanciated ViewAncestors. * - * @return The number of ViewRoots instanciated in the current process. + * @return The number of ViewAncestors instanciated in the current process. * * @hide */ - public static long getViewRootInstanceCount() { - return Debug.countInstancesOfClass(ViewRoot.class); + public static long getViewAncestorInstanceCount() { + return Debug.countInstancesOfClass(ViewAncestor.class); } /** @@ -558,6 +532,7 @@ public class ViewDebug { recyclerDump = new File(recyclerDump, sRecyclerTracePrefix + ".traces"); try { if (recyclerDump.exists()) { + //noinspection ResultOfMethodCallIgnored recyclerDump.delete(); } final FileOutputStream file = new FileOutputStream(recyclerDump); @@ -655,7 +630,7 @@ public class ViewDebug { return; } - sHierarhcyRoot = (ViewRoot) view.getRootView().getParent(); + sHierarhcyRoot = (ViewAncestor) view.getRootView().getParent(); } /** @@ -716,146 +691,6 @@ public class ViewDebug { sHierarhcyRoot = null; } - /** - * Outputs a trace to the currently opened traces file. The trace contains the class name - * and instance's hashcode of the specified view as well as the supplied trace type. - * - * @param view the view to trace - * @param event the event of the trace - * @param type the type of the trace - * - * @hide - */ - public static void trace(View view, MotionEvent event, MotionEventTraceType type) { - if (sMotionEventTraces == null) { - return; - } - - try { - sMotionEventTraces.write(type.name()); - sMotionEventTraces.write(' '); - sMotionEventTraces.write(event.getAction()); - sMotionEventTraces.write(' '); - sMotionEventTraces.write(view.getClass().getName()); - sMotionEventTraces.write('@'); - sMotionEventTraces.write(Integer.toHexString(view.hashCode())); - sHierarchyTraces.newLine(); - } catch (IOException e) { - Log.w("View", "Error while dumping trace of event " + event + " for view " + view); - } - } - - /** - * Starts tracing the motion events for the hierarchy of the specificy view. - * The trace is identified by a prefix, used to build the traces files names: - * <code>/EXTERNAL/motion-events/PREFIX.traces</code> and - * <code>/EXTERNAL/motion-events/PREFIX.tree</code>. - * - * Only one view hierarchy can be traced at the same time. After calling this method, any - * other invocation will result in a <code>IllegalStateException</code> unless - * {@link #stopMotionEventTracing()} is invoked before. - * - * Calling this method creates the file <code>/EXTERNAL/motion-events/PREFIX.traces</code> - * containing all the traces (or method calls) relative to the specified view's hierarchy. - * - * This method will return immediately if TRACE_HIERARCHY is false. - * - * @param prefix the traces files name prefix - * @param view the view whose hierarchy must be traced - * - * @see #stopMotionEventTracing() - * @see #trace(View, MotionEvent, android.view.ViewDebug.MotionEventTraceType) - * - * @hide - */ - public static void startMotionEventTracing(String prefix, View view) { - //noinspection PointlessBooleanExpression,ConstantConditions - if (!TRACE_MOTION_EVENTS) { - return; - } - - if (sMotionEventRoot != null) { - throw new IllegalStateException("You must call stopMotionEventTracing() before running" + - " a new trace!"); - } - - File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "motion-events/"); - //noinspection ResultOfMethodCallIgnored - hierarchyDump.mkdirs(); - - hierarchyDump = new File(hierarchyDump, prefix + ".traces"); - sMotionEventTracePrefix = prefix; - - try { - sMotionEventTraces = new BufferedWriter(new FileWriter(hierarchyDump), 32 * 1024); - } catch (IOException e) { - Log.e("View", "Could not dump view hierarchy"); - return; - } - - sMotionEventRoot = (ViewRoot) view.getRootView().getParent(); - } - - /** - * Stops the current motion events tracing. This method closes the file - * <code>/EXTERNAL/motion-events/PREFIX.traces</code>. - * - * Calling this method creates the file <code>/EXTERNAL/motion-events/PREFIX.tree</code> - * containing the view hierarchy of the view supplied to - * {@link #startMotionEventTracing(String, View)}. - * - * This method will return immediately if TRACE_HIERARCHY is false. - * - * @see #startMotionEventTracing(String, View) - * @see #trace(View, MotionEvent, android.view.ViewDebug.MotionEventTraceType) - * - * @hide - */ - public static void stopMotionEventTracing() { - //noinspection PointlessBooleanExpression,ConstantConditions - if (!TRACE_MOTION_EVENTS) { - return; - } - - if (sMotionEventRoot == null || sMotionEventTraces == null) { - throw new IllegalStateException("You must call startMotionEventTracing() before" + - " stopMotionEventTracing()!"); - } - - try { - sMotionEventTraces.close(); - } catch (IOException e) { - Log.e("View", "Could not write view traces"); - } - sMotionEventTraces = null; - - File hierarchyDump = new File(Environment.getExternalStorageDirectory(), "motion-events/"); - //noinspection ResultOfMethodCallIgnored - hierarchyDump.mkdirs(); - hierarchyDump = new File(hierarchyDump, sMotionEventTracePrefix + ".tree"); - - BufferedWriter out; - try { - out = new BufferedWriter(new FileWriter(hierarchyDump), 8 * 1024); - } catch (IOException e) { - Log.e("View", "Could not dump view hierarchy"); - return; - } - - View view = sMotionEventRoot.getView(); - if (view instanceof ViewGroup) { - ViewGroup group = (ViewGroup) view; - dumpViewHierarchy(group, out, 0); - try { - out.close(); - } catch (IOException e) { - Log.e("View", "Could not dump view hierarchy"); - } - } - - sHierarhcyRoot = null; - } - static void dispatchCommand(View view, String command, String parameters, OutputStream clientStream) throws IOException { @@ -870,6 +705,8 @@ public class ViewDebug { final String[] params = parameters.split(" "); if (REMOTE_COMMAND_CAPTURE.equalsIgnoreCase(command)) { capture(view, clientStream, params[0]); + } else if (REMOTE_COMMAND_OUTPUT_DISPLAYLIST.equalsIgnoreCase(command)) { + outputDisplayList(view, params[0]); } else if (REMOTE_COMMAND_INVALIDATE.equalsIgnoreCase(command)) { invalidate(view, params[0]); } else if (REMOTE_COMMAND_REQUEST_LAYOUT.equalsIgnoreCase(command)) { @@ -1051,8 +888,10 @@ public class ViewDebug { try { T[] data = operation.pre(); long start = Debug.threadCpuTimeNanos(); + //noinspection unchecked operation.run(data); duration[0] = Debug.threadCpuTimeNanos() - start; + //noinspection unchecked operation.post(data); } finally { latch.countDown(); @@ -1141,6 +980,11 @@ public class ViewDebug { } } + private static void outputDisplayList(View root, String parameter) throws IOException { + final View view = findView(root, parameter); + view.getViewAncestor().outputDisplayList(view); + } + private static void capture(View root, final OutputStream clientStream, String parameter) throws IOException { @@ -1178,12 +1022,7 @@ public class ViewDebug { cache[0] = captureView.createSnapshot( Bitmap.Config.ARGB_8888, 0, skpiChildren); } catch (OutOfMemoryError e) { - try { - cache[0] = captureView.createSnapshot( - Bitmap.Config.ARGB_4444, 0, skpiChildren); - } catch (OutOfMemoryError e2) { - Log.w("View", "Out of memory for bitmap"); - } + Log.w("View", "Out of memory for bitmap"); } finally { latch.countDown(); } @@ -1293,7 +1132,6 @@ public class ViewDebug { } final HashMap<Class<?>, Field[]> map = sFieldsForClasses; - final HashMap<AccessibleObject, ExportedProperty> annotations = sAnnotations; Field[] fields = map.get(klass); if (fields != null) { @@ -1309,7 +1147,7 @@ public class ViewDebug { if (field.isAnnotationPresent(ExportedProperty.class)) { field.setAccessible(true); foundFields.add(field); - annotations.put(field, field.getAnnotation(ExportedProperty.class)); + sAnnotations.put(field, field.getAnnotation(ExportedProperty.class)); } } @@ -1328,7 +1166,6 @@ public class ViewDebug { } final HashMap<Class<?>, Method[]> map = sMethodsForClasses; - final HashMap<AccessibleObject, ExportedProperty> annotations = sAnnotations; Method[] methods = map.get(klass); if (methods != null) { @@ -1346,7 +1183,7 @@ public class ViewDebug { method.getReturnType() != Void.class) { method.setAccessible(true); foundMethods.add(method); - annotations.put(method, method.getAnnotation(ExportedProperty.class)); + sAnnotations.put(method, method.getAnnotation(ExportedProperty.class)); } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 8dc86ac..57ee8a0 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -35,6 +35,7 @@ import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -129,11 +130,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // First touch target in the linked list of touch targets. private TouchTarget mFirstTouchTarget; - // Temporary arrays for splitting pointers. - private int[] mTmpPointerIndexMap; - private int[] mTmpPointerIds; - private MotionEvent.PointerCoords[] mTmpPointerCoords; - // For debugging only. You can see these in hierarchyviewer. @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) @ViewDebug.ExportedProperty(category = "events") @@ -147,6 +143,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager @ViewDebug.ExportedProperty(category = "events") private float mLastTouchDownY; + // Child which last received ACTION_HOVER_ENTER and ACTION_HOVER_MOVE. + private View mHoveredChild; + /** * Internal flags. * @@ -533,7 +532,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // note: knowing that mFocused is non-null is not a good enough reason // to break the traversal since in that case we'd actually have to find // the focused view and make sure it wasn't FOCUS_AFTER_DESCENDANTS and - // an ancestor of v; this will get checked for at ViewRoot + // an ancestor of v; this will get checked for at ViewAncestor && !(isFocused() && getDescendantFocusability() != FOCUS_AFTER_DESCENDANTS)) { mParent.focusableViewAvailable(v); } @@ -583,6 +582,36 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * {@inheritDoc} */ + public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) { + ViewParent parent = getParent(); + if (parent == null) { + return false; + } + final boolean propagate = onRequestSendAccessibilityEvent(child, event); + //noinspection SimplifiableIfStatement + if (!propagate) { + return false; + } + return parent.requestSendAccessibilityEvent(this, event); + } + + /** + * Called when a child has requested sending an {@link AccessibilityEvent} and + * gives an opportunity to its parent to augment the event. + * + * @param child The child which requests sending the event. + * @param event The event to be sent. + * @return True if the event should be sent. + * + * @see #requestSendAccessibilityEvent(View, AccessibilityEvent) + */ + public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { + return true; + } + + /** + * {@inheritDoc} + */ @Override public boolean dispatchUnhandledMove(View focused, int direction) { return mFocused != null && @@ -744,6 +773,18 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + @Override + public void findViewsWithText(ArrayList<View> outViews, CharSequence text) { + final int childrenCount = mChildrenCount; + final View[] children = mChildren; + for (int i = 0; i < childrenCount; i++) { + View child = children[i]; + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { + child.findViewsWithText(outViews, text); + } + } + } + /** * {@inheritDoc} */ @@ -904,7 +945,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final float tx = event.mX; final float ty = event.mY; - ViewRoot root = getViewRoot(); + ViewAncestor root = getViewAncestor(); // Dispatch down the view hierarchy switch (event.mAction) { @@ -926,6 +967,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 +988,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 +1020,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 +1032,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 +1064,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 +1106,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 +1122,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 +1164,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 +1202,70 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @Override public boolean dispatchTrackballEvent(MotionEvent event) { + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onTrackballEvent(event, 1); + } + if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { - return super.dispatchTrackballEvent(event); + if (super.dispatchTrackballEvent(event)) { + return true; + } } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) { - return mFocused.dispatchTrackballEvent(event); + if (mFocused.dispatchTrackballEvent(event)) { + return true; + } + } + + if (mInputEventConsistencyVerifier != null) { + mInputEventConsistencyVerifier.onUnhandledEvent(event, 1); } return false; } - /** - * {@inheritDoc} - */ + /** @hide */ @Override - public boolean dispatchGenericMotionEvent(MotionEvent event) { - if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { - // Send the event to the child under the pointer. + protected boolean dispatchHoverEvent(MotionEvent event) { + // Send the hover enter or hover move event to the view group first. + // If it handles the event then a hovered child should receive hover exit. + boolean handled = false; + final boolean interceptHover; + final int action = event.getAction(); + if (action == MotionEvent.ACTION_HOVER_EXIT) { + interceptHover = true; + } else { + handled = super.dispatchHoverEvent(event); + interceptHover = handled; + } + + // Send successive hover events to the hovered child as long as the pointer + // remains within the child's bounds. + MotionEvent eventNoHistory = event; + if (mHoveredChild != null) { + final float x = event.getX(); + final float y = event.getY(); + + if (interceptHover + || !isTransformedTouchPointInView(x, y, mHoveredChild, null)) { + // Pointer exited the child. + // Send it a hover exit with only the most recent coordinates. We could + // try to find the exact point in history when the pointer left the view + // but it is not worth the effort. + eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory); + eventNoHistory.setAction(MotionEvent.ACTION_HOVER_EXIT); + handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, mHoveredChild); + eventNoHistory.setAction(action); + mHoveredChild = null; + } else { + // Pointer is still within the child. + //noinspection ConstantConditions + handled |= dispatchTransformedGenericPointerEvent(event, mHoveredChild); + } + } + + // Find a new hovered child if needed. + if (!interceptHover && mHoveredChild == null + && (action == MotionEvent.ACTION_HOVER_ENTER + || action == MotionEvent.ACTION_HOVER_MOVE)) { final int childrenCount = mChildrenCount; if (childrenCount != 0) { final View[] children = mChildren; @@ -1155,45 +1274,100 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = childrenCount - 1; i >= 0; i--) { final View child = children[i]; - if ((child.mViewFlags & VISIBILITY_MASK) != VISIBLE - && child.getAnimation() == null) { - // Skip invisible child unless it is animating. + if (!canViewReceivePointerEvents(child) + || !isTransformedTouchPointInView(x, y, child, null)) { continue; } - if (!isTransformedTouchPointInView(x, y, child, null)) { - // Scroll point is out of child's bounds. - continue; + // Found the hovered child. + mHoveredChild = child; + if (action == MotionEvent.ACTION_HOVER_MOVE) { + // Pointer was moving within the view group and entered the child. + // Send it a hover enter and hover move with only the most recent + // coordinates. We could try to find the exact point in history when + // the pointer entered the view but it is not worth the effort. + eventNoHistory = obtainMotionEventNoHistoryOrSelf(eventNoHistory); + eventNoHistory.setAction(MotionEvent.ACTION_HOVER_ENTER); + handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child); + eventNoHistory.setAction(action); + + handled |= dispatchTransformedGenericPointerEvent(eventNoHistory, child); + } else { /* must be ACTION_HOVER_ENTER */ + // Pointer entered the child. + handled |= dispatchTransformedGenericPointerEvent(event, child); } + break; + } + } + } - final float offsetX = mScrollX - child.mLeft; - final float offsetY = mScrollY - child.mTop; - final boolean handled; - if (!child.hasIdentityMatrix()) { - MotionEvent transformedEvent = MotionEvent.obtain(event); - transformedEvent.offsetLocation(offsetX, offsetY); - transformedEvent.transform(child.getInverseMatrix()); - handled = child.dispatchGenericMotionEvent(transformedEvent); - transformedEvent.recycle(); - } else { - event.offsetLocation(offsetX, offsetY); - handled = child.dispatchGenericMotionEvent(event); - event.offsetLocation(-offsetX, -offsetY); - } + // Recycle the copy of the event that we made. + if (eventNoHistory != event) { + eventNoHistory.recycle(); + } - if (handled) { - return true; - } + // Send hover exit to the view group. If there was a child, we will already have + // sent the hover exit to it. + if (action == MotionEvent.ACTION_HOVER_EXIT) { + handled |= super.dispatchHoverEvent(event); + } + + // Done. + return handled; + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + // Handle the event only if leaf. This guarantees that + // the leafs (or any custom class that returns true from + // this method) will get a change to process the hover. + //noinspection SimplifiableIfStatement + if (getChildCount() == 0) { + return super.onHoverEvent(event); + } + return false; + } + + private static MotionEvent obtainMotionEventNoHistoryOrSelf(MotionEvent event) { + if (event.getHistorySize() == 0) { + return event; + } + return MotionEvent.obtainNoHistory(event); + } + + /** @hide */ + @Override + protected boolean dispatchGenericPointerEvent(MotionEvent event) { + // Send the event to the child under the pointer. + final int childrenCount = mChildrenCount; + if (childrenCount != 0) { + final View[] children = mChildren; + final float x = event.getX(); + final float y = event.getY(); + + for (int i = childrenCount - 1; i >= 0; i--) { + final View child = children[i]; + if (!canViewReceivePointerEvents(child) + || !isTransformedTouchPointInView(x, y, child, null)) { + continue; } - } - // No child handled the event. Send it to this view group. - return super.dispatchGenericMotionEvent(event); + if (dispatchTransformedGenericPointerEvent(event, child)) { + return true; + } + } } + // No child handled the event. Send it to this view group. + return super.dispatchGenericPointerEvent(event); + } + + /** @hide */ + @Override + protected boolean dispatchGenericFocusedEvent(MotionEvent event) { // Send the event to the focused child or to this view group if it has focus. if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { - return super.dispatchGenericMotionEvent(event); + return super.dispatchGenericFocusedEvent(event); } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) { return mFocused.dispatchGenericMotionEvent(event); } @@ -1201,166 +1375,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 +1677,15 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Returns true if a child view can receive pointer events. + * @hide + */ + private static boolean canViewReceivePointerEvents(View child) { + return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE + || child.getAnimation() != null; + } + + /** * Returns true if a child view contains the specified point when transformed * into its coordinate space. * Child must not be null. @@ -1524,141 +1734,38 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } // Calculate the number of pointers to deliver. - final int oldPointerCount = event.getPointerCount(); - int newPointerCount = 0; - if (desiredPointerIdBits == TouchTarget.ALL_POINTER_IDS) { - newPointerCount = oldPointerCount; - } else { - for (int i = 0; i < oldPointerCount; i++) { - final int pointerId = event.getPointerId(i); - final int pointerIdBit = 1 << pointerId; - if ((pointerIdBit & desiredPointerIdBits) != 0) { - newPointerCount += 1; - } - } - } + final int oldPointerIdBits = event.getPointerIdBits(); + final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; // If for some reason we ended up in an inconsistent state where it looks like we // might produce a motion event with no pointers in it, then drop the event. - if (newPointerCount == 0) { + if (newPointerIdBits == 0) { return false; } // If the number of pointers is the same and we don't need to perform any fancy // irreversible transformations, then we can reuse the motion event for this // dispatch as long as we are careful to revert any changes we make. - final boolean reuse = newPointerCount == oldPointerCount - && (child == null || child.hasIdentityMatrix()); - if (reuse) { - if (child == null) { - handled = super.dispatchTouchEvent(event); - } else { - final float offsetX = mScrollX - child.mLeft; - final float offsetY = mScrollY - child.mTop; - event.offsetLocation(offsetX, offsetY); - - handled = child.dispatchTouchEvent(event); - - event.offsetLocation(-offsetX, -offsetY); - } - return handled; - } - - // Make a copy of the event. - // If the number of pointers is different, then we need to filter out irrelevant pointers - // as we make a copy of the motion event. - MotionEvent transformedEvent; - if (newPointerCount == oldPointerCount) { - transformedEvent = MotionEvent.obtain(event); - } else { - growTmpPointerArrays(newPointerCount); - final int[] newPointerIndexMap = mTmpPointerIndexMap; - final int[] newPointerIds = mTmpPointerIds; - final MotionEvent.PointerCoords[] newPointerCoords = mTmpPointerCoords; - - int newPointerIndex = 0; - int oldPointerIndex = 0; - while (newPointerIndex < newPointerCount) { - final int pointerId = event.getPointerId(oldPointerIndex); - final int pointerIdBits = 1 << pointerId; - if ((pointerIdBits & desiredPointerIdBits) != 0) { - newPointerIndexMap[newPointerIndex] = oldPointerIndex; - newPointerIds[newPointerIndex] = pointerId; - if (newPointerCoords[newPointerIndex] == null) { - newPointerCoords[newPointerIndex] = new MotionEvent.PointerCoords(); - } - - newPointerIndex += 1; - } - oldPointerIndex += 1; - } - - final int newAction; - if (cancel) { - newAction = MotionEvent.ACTION_CANCEL; - } else { - final int oldMaskedAction = oldAction & MotionEvent.ACTION_MASK; - if (oldMaskedAction == MotionEvent.ACTION_POINTER_DOWN - || oldMaskedAction == MotionEvent.ACTION_POINTER_UP) { - final int changedPointerId = event.getPointerId( - (oldAction & MotionEvent.ACTION_POINTER_INDEX_MASK) - >> MotionEvent.ACTION_POINTER_INDEX_SHIFT); - final int changedPointerIdBits = 1 << changedPointerId; - if ((changedPointerIdBits & desiredPointerIdBits) != 0) { - if (newPointerCount == 1) { - // The first/last pointer went down/up. - newAction = oldMaskedAction == MotionEvent.ACTION_POINTER_DOWN - ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP; - } else { - // A secondary pointer went down/up. - int newChangedPointerIndex = 0; - while (newPointerIds[newChangedPointerIndex] != changedPointerId) { - newChangedPointerIndex += 1; - } - newAction = oldMaskedAction | (newChangedPointerIndex - << MotionEvent.ACTION_POINTER_INDEX_SHIFT); - } - } else { - // An unrelated pointer changed. - newAction = MotionEvent.ACTION_MOVE; - } + // Otherwise we need to make a copy. + final MotionEvent transformedEvent; + if (newPointerIdBits == oldPointerIdBits) { + if (child == null || child.hasIdentityMatrix()) { + if (child == null) { + handled = super.dispatchTouchEvent(event); } else { - // Simple up/down/cancel/move motion action. - newAction = oldMaskedAction; - } - } + final float offsetX = mScrollX - child.mLeft; + final float offsetY = mScrollY - child.mTop; + event.offsetLocation(offsetX, offsetY); - transformedEvent = null; - final int historySize = event.getHistorySize(); - for (int historyIndex = 0; historyIndex <= historySize; historyIndex++) { - for (newPointerIndex = 0; newPointerIndex < newPointerCount; newPointerIndex++) { - final MotionEvent.PointerCoords c = newPointerCoords[newPointerIndex]; - oldPointerIndex = newPointerIndexMap[newPointerIndex]; - if (historyIndex != historySize) { - event.getHistoricalPointerCoords(oldPointerIndex, historyIndex, c); - } else { - event.getPointerCoords(oldPointerIndex, c); - } - } + handled = child.dispatchTouchEvent(event); - final long eventTime; - if (historyIndex != historySize) { - eventTime = event.getHistoricalEventTime(historyIndex); - } else { - eventTime = event.getEventTime(); - } - - if (transformedEvent == null) { - transformedEvent = MotionEvent.obtain( - event.getDownTime(), eventTime, newAction, - newPointerCount, newPointerIds, newPointerCoords, - event.getMetaState(), event.getXPrecision(), event.getYPrecision(), - event.getDeviceId(), event.getEdgeFlags(), event.getSource(), - event.getFlags()); - } else { - transformedEvent.addBatch(eventTime, newPointerCoords, 0); + event.offsetLocation(-offsetX, -offsetY); } + return handled; } + transformedEvent = MotionEvent.obtain(event); + } else { + transformedEvent = event.split(newPointerIdBits); } // Perform any necessary transformations and dispatch. @@ -1681,36 +1788,6 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Enlarge the temporary pointer arrays for splitting pointers. - * May discard contents (but keeps PointerCoords objects to avoid reallocating them). - */ - private void growTmpPointerArrays(int desiredCapacity) { - final MotionEvent.PointerCoords[] oldTmpPointerCoords = mTmpPointerCoords; - int capacity; - if (oldTmpPointerCoords != null) { - capacity = oldTmpPointerCoords.length; - if (desiredCapacity <= capacity) { - return; - } - } else { - capacity = 4; - } - - while (capacity < desiredCapacity) { - capacity *= 2; - } - - mTmpPointerIndexMap = new int[capacity]; - mTmpPointerIds = new int[capacity]; - mTmpPointerCoords = new MotionEvent.PointerCoords[capacity]; - - if (oldTmpPointerCoords != null) { - System.arraycopy(oldTmpPointerCoords, 0, mTmpPointerCoords, 0, - oldTmpPointerCoords.length); - } - } - - /** * Enable or disable the splitting of MotionEvents to multiple children during touch event * dispatch. This behavior is enabled by default for applications that target an * SDK version of {@link Build.VERSION_CODES#HONEYCOMB} or newer. @@ -1818,7 +1895,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @see #FOCUS_BEFORE_DESCENDANTS * @see #FOCUS_AFTER_DESCENDANTS * @see #FOCUS_BLOCK_DESCENDANTS - * @see #onRequestFocusInDescendants + * @see #onRequestFocusInDescendants(int, android.graphics.Rect) */ @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { @@ -1931,11 +2008,26 @@ 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 false; + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + for (int i = 0, count = mChildrenCount; i < count; i++) { + View child = mChildren[i]; + info.addChild(child); } - return populated; } /** @@ -1975,7 +2067,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager public void setPadding(int left, int top, int right, int bottom) { super.setPadding(left, top, right, bottom); - if ((mPaddingLeft | mPaddingTop | mPaddingRight | mPaddingRight) != 0) { + if ((mPaddingLeft | mPaddingTop | mPaddingRight | mPaddingBottom) != 0) { mGroupFlags |= FLAG_PADDING_NOT_NULL; } else { mGroupFlags &= ~FLAG_PADDING_NOT_NULL; @@ -1999,10 +2091,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Perform dispatching of a {@link #saveHierarchyState freeze()} to only this view, - * not to its children. For use when overriding - * {@link #dispatchSaveInstanceState dispatchFreeze()} to allow subclasses to freeze - * their own state but not the state of their children. + * Perform dispatching of a {@link #saveHierarchyState(android.util.SparseArray)} freeze()} + * to only this view, not to its children. For use when overriding + * {@link #dispatchSaveInstanceState(android.util.SparseArray)} dispatchFreeze()} to allow + * subclasses to freeze their own state but not the state of their children. * * @param container the container */ @@ -2027,10 +2119,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** - * Perform dispatching of a {@link #restoreHierarchyState thaw()} to only this view, - * not to its children. For use when overriding - * {@link #dispatchRestoreInstanceState dispatchThaw()} to allow subclasses to thaw - * their own state but not the state of their children. + * Perform dispatching of a {@link #restoreHierarchyState(android.util.SparseArray)} + * to only this view, not to its children. For use when overriding + * {@link #dispatchRestoreInstanceState(android.util.SparseArray)} to allow + * subclasses to thaw their own state but not the state of their children. * * @param container the container */ @@ -3244,6 +3336,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 +3403,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 +3417,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 +3478,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 +3491,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager mTransition.removeChild(this, view); } + if (view == hoveredChild) { + mHoveredChild = null; + } + if (view == focused) { view.clearFocusForRemoval(); clearChildFocus = view; @@ -3610,13 +3716,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (drawAnimation) { if (view != null) { view.mPrivateFlags |= DRAW_ANIMATION; - } else if (parent instanceof ViewRoot) { - ((ViewRoot) parent).mIsAnimating = true; + } else if (parent instanceof ViewAncestor) { + ((ViewAncestor) parent).mIsAnimating = true; } } - if (parent instanceof ViewRoot) { - ((ViewRoot) parent).invalidate(); + if (parent instanceof ViewAncestor) { + ((ViewAncestor) parent).invalidate(); parent = null; } else if (view != null) { if ((view.mPrivateFlags & DRAWN) == DRAWN || @@ -3671,8 +3777,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (drawAnimation) { if (view != null) { view.mPrivateFlags |= DRAW_ANIMATION; - } else if (parent instanceof ViewRoot) { - ((ViewRoot) parent).mIsAnimating = true; + } else if (parent instanceof ViewAncestor) { + ((ViewAncestor) parent).mIsAnimating = true; } } @@ -4195,7 +4301,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // If this group is dirty, check that the parent is dirty as well if ((mPrivateFlags & DIRTY_MASK) != 0) { final ViewParent parent = getParent(); - if (parent != null && !(parent instanceof ViewRoot)) { + if (parent != null && !(parent instanceof ViewAncestor)) { if ((((View) parent).mPrivateFlags & DIRTY_MASK) == 0) { result = false; android.util.Log.d(ViewDebug.CONSISTENCY_LOG_TAG, @@ -4755,6 +4861,33 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * This method is called by LayoutTransition when there are 'changing' animations that need + * to start after the layout/setup phase. The request is forwarded to the ViewAncestor, who + * starts all pending transitions prior to the drawing phase in the current traversal. + * + * @param transition The LayoutTransition to be started on the next traversal. + * + * @hide + */ + public void requestTransitionStart(LayoutTransition transition) { + ViewAncestor viewAncestor = getViewAncestor(); + viewAncestor.requestTransitionStart(transition); + } + + /** + * Return true if the pressed state should be delayed for children or descendants of this + * ViewGroup. Generally, this should be done for containers that can scroll, such as a List. + * This prevents the pressed state from appearing when the user is actually trying to scroll + * the content. + * + * The default implementation returns true for compatibility reasons. Subclasses that do + * not scroll should generally override this method and return false. + */ + public boolean shouldDelayChildPressedState() { + return true; + } + + /** * LayoutParams are used by views to tell their parents how they want to be * laid out. See * {@link android.R.styleable#ViewGroup_Layout ViewGroup Layout Attributes} diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index d7d4c3f..655df39 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -17,6 +17,7 @@ package android.view; import android.graphics.Rect; +import android.view.accessibility.AccessibilityEvent; /** * Defines the responsibilities for a class that will be a parent of a View. @@ -222,4 +223,22 @@ public interface ViewParent { */ public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate); + + /** + * Called by a child to request from its parent to send an {@link AccessibilityEvent}. + * The child has already populated a record for itself in the event and is delegating + * to its parent to send the event. The parent can optionally add a record for itself. + * <p> + * Note: An accessibility event is fired by an individual view which populates the + * event with a record for its state and requests from its parent to perform + * the sending. The parent can optionally add a record for itself before + * dispatching the request to its parent. A parent can also choose not to + * respect the request for sending the event. The accessibility event is sent + * by the topmost view in the view tree. + * + * @param child The child which requests sending the event. + * @param event The event to be sent. + * @return True if the event was sent. + */ + public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event); } diff --git a/core/java/android/view/ViewPropertyAnimator.java b/core/java/android/view/ViewPropertyAnimator.java index 1d56e9d..9eddf23 100644 --- a/core/java/android/view/ViewPropertyAnimator.java +++ b/core/java/android/view/ViewPropertyAnimator.java @@ -67,6 +67,19 @@ public class ViewPropertyAnimator { private boolean mDurationSet = false; /** + * The startDelay of the underlying Animator object. By default, we don't set the startDelay + * on the Animator and just use its default startDelay. If the startDelay is ever set on this + * Animator, then we use the startDelay that it was set to. + */ + private long mStartDelay = 0; + + /** + * A flag indicating whether the startDelay has been set on this object. If not, we don't set + * the startDelay on the underlying Animator, but instead just use its default startDelay. + */ + private boolean mStartDelaySet = false; + + /** * The interpolator of the underlying Animator object. By default, we don't set the interpolator * on the Animator and just use its default interpolator. If the interpolator is ever set on * this Animator, then we use the interpolator that it was set to. @@ -233,6 +246,60 @@ public class ViewPropertyAnimator { } /** + * Returns the current duration of property animations. If the duration was set on this + * object, that value is returned. Otherwise, the default value of the underlying Animator + * is returned. + * + * @see #setDuration(long) + * @return The duration of animations, in milliseconds. + */ + public long getDuration() { + if (mStartDelaySet) { + return mStartDelay; + } else { + // Just return the default from ValueAnimator, since that's what we'd get if + // the value has not been set otherwise + return new ValueAnimator().getDuration(); + } + } + + /** + * Returns the current startDelay of property animations. If the startDelay was set on this + * object, that value is returned. Otherwise, the default value of the underlying Animator + * is returned. + * + * @see #setStartDelay(long) + * @return The startDelay of animations, in milliseconds. + */ + public long getStartDelay() { + if (mStartDelaySet) { + return mStartDelay; + } else { + // Just return the default from ValueAnimator (0), since that's what we'd get if + // the value has not been set otherwise + return 0; + } + } + + /** + * Sets the startDelay for the underlying animator that animates the requested properties. + * By default, the animator uses the default value for ValueAnimator. Calling this method + * will cause the declared value to be used instead. + * @param startDelay The delay of ensuing property animations, in milliseconds. The value + * cannot be negative. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimator setStartDelay(long startDelay) { + if (startDelay < 0) { + throw new IllegalArgumentException("Animators cannot have negative duration: " + + startDelay); + } + mStartDelaySet = true; + mStartDelay = startDelay; + return this; + } + + /** * Sets the interpolator for the underlying animator that animates the requested properties. * By default, the animator uses the default interpolator for ValueAnimator. Calling this method * will cause the declared object to be used instead. @@ -259,6 +326,33 @@ public class ViewPropertyAnimator { } /** + * Starts the currently pending property animations immediately. Calling <code>start()</code> + * is optional because all animations start automatically at the next opportunity. However, + * if the animations are needed to start immediately and synchronously (not at the time when + * the next event is processed by the hierarchy, which is when the animations would begin + * otherwise), then this method can be used. + */ + public void start() { + startAnimation(); + } + + /** + * Cancels all property animations that are currently running or pending. + */ + public void cancel() { + if (mAnimatorMap.size() > 0) { + HashMap<Animator, PropertyBundle> mAnimatorMapCopy = + (HashMap<Animator, PropertyBundle>)mAnimatorMap.clone(); + Set<Animator> animatorSet = mAnimatorMapCopy.keySet(); + for (Animator runningAnim : animatorSet) { + runningAnim.cancel(); + } + } + mPendingAnimations.clear(); + mView.getHandler().removeCallbacks(mAnimationStarter); + } + + /** * This method will cause the View's <code>x</code> property to be animated to the * specified value. Animations already running on the property will be canceled. * @@ -598,7 +692,7 @@ public class ViewPropertyAnimator { // on a property will cancel a previous animation on that property, so // there can only ever be one such animation running. if (bundle.mPropertyMask == NONE) { - // the animation is not longer changing anything - cancel it + // the animation is no longer changing anything - cancel it animatorToCancel = runningAnim; break; } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 8a18aaf..b0181bb 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -77,8 +77,8 @@ public interface WindowManager extends ViewManager { implements Parcelable { /** * X position for this window. With the default gravity it is ignored. - * When using {@link Gravity#LEFT} or {@link Gravity#RIGHT} it provides - * an offset from the given edge. + * When using {@link Gravity#LEFT} or {@link Gravity#START} or {@link Gravity#RIGHT} or + * {@link Gravity#END} it provides an offset from the given edge. */ @ViewDebug.ExportedProperty public int x; @@ -388,6 +388,12 @@ public interface WindowManager extends ViewManager { public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18; /** + * Window type: Navigation bar (when distinct from status bar) + * @hide + */ + public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19; + + /** * End of types of system windows. */ public static final int LAST_SYSTEM_WINDOW = 2999; diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index 02ab1dc..54e7c04 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -23,7 +23,6 @@ import android.content.res.Configuration; import android.graphics.PixelFormat; import android.os.IBinder; import android.util.AndroidRuntimeException; -import android.util.Config; import android.util.Log; import android.util.Slog; import android.view.WindowManager; @@ -81,7 +80,7 @@ public class WindowManagerImpl implements WindowManager { public static final int ADD_PERMISSION_DENIED = -8; private View[] mViews; - private ViewRoot[] mRoots; + private ViewAncestor[] mRoots; private WindowManager.LayoutParams[] mParams; private final static Object sLock = new Object(); @@ -193,13 +192,9 @@ public class WindowManagerImpl implements WindowManager { addView(view, params, cih, false); } - public void addViewNesting(View view, ViewGroup.LayoutParams params) { - addView(view, params, null, false); - } - private void addView(View view, ViewGroup.LayoutParams params, CompatibilityInfoHolder cih, 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( @@ -209,7 +204,7 @@ public class WindowManagerImpl implements WindowManager { final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; - ViewRoot root; + ViewAncestor root; View panelParentView = null; synchronized (this) { @@ -246,7 +241,7 @@ public class WindowManagerImpl implements WindowManager { } } - root = new ViewRoot(view.getContext()); + root = new ViewAncestor(view.getContext()); root.mAddNesting = 1; if (cih == null) { root.mCompatibilityInfo = new CompatibilityInfoHolder(); @@ -259,7 +254,7 @@ public class WindowManagerImpl implements WindowManager { if (mViews == null) { index = 1; mViews = new View[1]; - mRoots = new ViewRoot[1]; + mRoots = new ViewAncestor[1]; mParams = new WindowManager.LayoutParams[1]; } else { index = mViews.length + 1; @@ -267,7 +262,7 @@ public class WindowManagerImpl implements WindowManager { mViews = new View[index]; System.arraycopy(old, 0, mViews, 0, index-1); old = mRoots; - mRoots = new ViewRoot[index]; + mRoots = new ViewAncestor[index]; System.arraycopy(old, 0, mRoots, 0, index-1); old = mParams; mParams = new WindowManager.LayoutParams[index]; @@ -295,7 +290,7 @@ public class WindowManagerImpl implements WindowManager { synchronized (this) { int index = findViewLocked(view, true); - ViewRoot root = mRoots[index]; + ViewAncestor root = mRoots[index]; mParams[index] = wparams; root.setLayoutParams(wparams, false); } @@ -310,14 +305,14 @@ public class WindowManagerImpl implements WindowManager { } throw new IllegalStateException("Calling with view " + view - + " but the ViewRoot is attached to " + curView); + + " but the ViewAncestor is attached to " + curView); } } public void removeViewImmediate(View view) { synchronized (this) { int index = findViewLocked(view, true); - ViewRoot root = mRoots[index]; + ViewAncestor root = mRoots[index]; View curView = root.getView(); root.mAddNesting = 0; @@ -328,12 +323,12 @@ public class WindowManagerImpl implements WindowManager { } throw new IllegalStateException("Calling with view " + view - + " but the ViewRoot is attached to " + curView); + + " but the ViewAncestor is attached to " + curView); } } View removeViewLocked(int index) { - ViewRoot root = mRoots[index]; + ViewAncestor root = mRoots[index]; View view = root.getView(); // Don't really remove until we have matched all calls to add(). @@ -361,7 +356,7 @@ public class WindowManagerImpl implements WindowManager { removeItem(tmpViews, mViews, index); mViews = tmpViews; - ViewRoot[] tmpRoots = new ViewRoot[count-1]; + ViewAncestor[] tmpRoots = new ViewAncestor[count-1]; removeItem(tmpRoots, mRoots, index); mRoots = tmpRoots; @@ -388,7 +383,7 @@ public class WindowManagerImpl implements WindowManager { //Log.i("foo", "@ " + i + " token " + mParams[i].token // + " view " + mRoots[i].getView()); if (token == null || mParams[i].token == token) { - ViewRoot root = mRoots[i]; + ViewAncestor root = mRoots[i]; root.mAddNesting = 1; //Log.i("foo", "Force closing " + root); @@ -415,7 +410,7 @@ public class WindowManagerImpl implements WindowManager { int count = mViews.length; for (int i=0; i<count; i++) { if (token == null || mParams[i].token == token) { - ViewRoot root = mRoots[i]; + ViewAncestor root = mRoots[i]; root.setStopped(stopped); } } @@ -427,7 +422,7 @@ public class WindowManagerImpl implements WindowManager { int count = mViews.length; config = new Configuration(config); for (int i=0; i<count; i++) { - ViewRoot root = mRoots[i]; + ViewAncestor root = mRoots[i]; root.requestUpdateConfiguration(config); } } @@ -435,13 +430,13 @@ public class WindowManagerImpl implements WindowManager { public WindowManager.LayoutParams getRootViewLayoutParameter(View view) { ViewParent vp = view.getParent(); - while (vp != null && !(vp instanceof ViewRoot)) { + while (vp != null && !(vp instanceof ViewAncestor)) { vp = vp.getParent(); } if (vp == null) return null; - ViewRoot vr = (ViewRoot)vp; + ViewAncestor vr = (ViewAncestor)vp; int N = mRoots.length; for (int i = 0; i < N; ++i) { diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 4d4569c..8a30c7b 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -82,6 +82,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; @@ -889,7 +891,13 @@ public interface WindowManagerPolicy { public boolean performHapticFeedbackLw(WindowState win, int effectId, boolean always); /** - * Called when we have stopped keeping the screen on because a window + * Called when we have started keeping the screen on because a window + * requesting this has become visible. + */ + public void screenOnStartedLw(); + + /** + * Called when we have stopped keeping the screen on because the last window * requesting this is no longer visible. */ public void screenOnStoppedLw(); 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..06e4827 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -16,18 +16,34 @@ package android.view.accessibility; +import android.accessibilityservice.IAccessibilityServiceConnection; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; import android.text.TextUtils; +import android.view.View; 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 +161,8 @@ 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 { + private static final boolean DEBUG = false; /** * Invalid selection/focus position. @@ -159,7 +176,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 +224,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 +256,73 @@ public final class AccessibilityEvent implements Parcelable { */ public static final int TYPES_ALL_MASK = 0xFFFFFFFF; - private static final int MAX_POOL_SIZE = 2; - private static final Object mPoolLock = new Object(); + private static final int MAX_POOL_SIZE = 10; + private static final Object sPoolLock = new Object(); private static AccessibilityEvent sPool; private static int sPoolSize; - - private static final int CHECKED = 0x00000001; - private static final int ENABLED = 0x00000002; - private static final int PASSWORD = 0x00000004; - private static final int FULL_SCREEN = 0x00000080; - private AccessibilityEvent mNext; + private boolean mIsInPool; private int mEventType; - private int mBooleanProperties; - private int mCurrentItemIndex; - private int mItemCount; - private int mFromIndex; - private int mAddedCount; - private int mRemovedCount; - - private long mEventTime; - - private CharSequence mClassName; + private int mSourceAccessibilityViewId = View.NO_ID; + private int mSourceAccessibilityWindowId = View.NO_ID; private CharSequence mPackageName; - private CharSequence mContentDescription; - private CharSequence mBeforeText; - - private Parcelable mParcelableData; + private long mEventTime; - private final List<CharSequence> mText = new ArrayList<CharSequence>(); + private final ArrayList<AccessibilityRecord> mRecords = new ArrayList<AccessibilityRecord>(); - private boolean mIsInPool; + private IAccessibilityServiceConnection mConnection; /* * Hide constructor from clients. */ private AccessibilityEvent() { - mCurrentItemIndex = INVALID_POSITION; } /** - * Gets if the source is checked. + * Initialize an event from another one. * - * @return True if the view is checked, false otherwise. + * @param event The event to initialize from. */ - public boolean isChecked() { - return getBooleanProperty(CHECKED); + void init(AccessibilityEvent event) { + super.init(event); + mEventType = event.mEventType; + mEventTime = event.mEventTime; + mSourceAccessibilityWindowId = event.mSourceAccessibilityWindowId; + mSourceAccessibilityViewId = event.mSourceAccessibilityViewId; + mPackageName = event.mPackageName; + mConnection = event.mConnection; } /** - * Sets if the source is checked. + * Gets the number of records contained in the event. * - * @param isChecked True if the view is checked, false otherwise. + * @return The number of records. */ - public void setChecked(boolean isChecked) { - setBooleanProperty(CHECKED, isChecked); + public int getRecordCount() { + return mRecords.size(); } /** - * Gets if the source is enabled. + * Appends an {@link AccessibilityRecord} to the end of event records. * - * @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. + * @param record The record to append. * - * @return True if the view is a password field, false otherwise. + * @throws IllegalStateException If called from an AccessibilityService. */ - public boolean isPassword() { - return getBooleanProperty(PASSWORD); + public void appendRecord(AccessibilityRecord record) { + enforceNotSealed(); + mRecords.add(record); } /** - * Sets if the source is a password field. + * Gets the records at a given index. * - * @param isPassword True if the view is a password field, false otherwise. + * @param index The index. + * @return The records at the specified index. */ - public void setPassword(boolean isPassword) { - setBooleanProperty(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(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(FULL_SCREEN); + public AccessibilityRecord getRecord(int index) { + return mRecords.get(index); } /** @@ -336,102 +335,90 @@ public final class AccessibilityEvent implements Parcelable { } /** - * Sets the event type. + * Sets the event source. * - * @param eventType The event type. - */ - public void setEventType(int eventType) { - mEventType = eventType; - } - - /** - * Gets the number of items that can be visited. + * @param source The source. * - * @return The number of items. + * @throws IllegalStateException If called from an AccessibilityService. */ - public int getItemCount() { - return mItemCount; + public void setSource(View source) { + enforceNotSealed(); + if (source != null) { + mSourceAccessibilityWindowId = source.getAccessibilityWindowId(); + mSourceAccessibilityViewId = source.getAccessibilityViewId(); + } else { + mSourceAccessibilityWindowId = View.NO_ID; + mSourceAccessibilityViewId = View.NO_ID; + } } /** - * Sets the number of items that can be visited. - * - * @param itemCount The number of items. - */ - public void setItemCount(int itemCount) { - mItemCount = itemCount; + * Gets the {@link AccessibilityNodeInfo} of the event source. + * <p> + * <strong> + * It is a client responsibility to recycle the received info by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * @return The info. + */ + public AccessibilityNodeInfo getSource() { + enforceSealed(); + if (mSourceAccessibilityWindowId == View.NO_ID + || mSourceAccessibilityViewId == View.NO_ID) { + return null; + } + try { + return mConnection.findAccessibilityNodeInfoByAccessibilityId( + mSourceAccessibilityWindowId, mSourceAccessibilityViewId); + } catch (RemoteException e) { + return null; + } } /** - * Gets the index of the source in the list of items the can be visited. + * Gets the id of the window from which the event comes from. * - * @return The current item index. + * @return The window id. */ - public int getCurrentItemIndex() { - return mCurrentItemIndex; + public int getAccessibilityWindowId() { + return mSourceAccessibilityWindowId; } /** - * Sets the index of the source in the list of items that can be visited. + * Sets the client token for the accessibility service that + * provided this node info. * - * @param currentItemIndex The current item index. - */ - public void setCurrentItemIndex(int currentItemIndex) { - mCurrentItemIndex = currentItemIndex; - } - - /** - * Gets the index of the first character of the changed sequence. + * @param connection The connection. * - * @return The index of the first character. + * @hide */ - public int getFromIndex() { - return mFromIndex; + public final void setConnection(IAccessibilityServiceConnection connection) { + mConnection = connection; } /** - * Sets the index of the first character of the changed sequence. + * Gets the accessibility window id of the source window. * - * @param fromIndex The index of the first character. - */ - public void setFromIndex(int fromIndex) { - mFromIndex = fromIndex; - } - - /** - * Gets the number of added characters. + * @return The id. * - * @return The number of added characters. + * @hide */ - public int getAddedCount() { - return mAddedCount; + public int getSourceAccessibilityWindowId() { + return mSourceAccessibilityWindowId; } /** - * 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. + * Sets the event type. * - * @return The number of removed characters. - */ - public int getRemovedCount() { - return mRemovedCount; - } - - /** - * Sets the number of removed characters. + * @param eventType The event type. * - * @param removedCount The number of removed characters. + * @throws IllegalStateException If called from an AccessibilityService. */ - public void setRemovedCount(int removedCount) { - mRemovedCount = removedCount; + public void setEventType(int eventType) { + enforceNotSealed(); + mEventType = eventType; } /** @@ -447,30 +434,15 @@ public final class AccessibilityEvent implements Parcelable { * Sets the time in which this event was sent. * * @param eventTime The event time. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setEventTime(long eventTime) { + enforceNotSealed(); mEventTime = eventTime; } /** - * 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. @@ -483,76 +455,15 @@ public final class AccessibilityEvent implements Parcelable { * Sets the package name of the source. * * @param packageName The package name. + * + * @throws IllegalStateException If called from an AccessibilityService. */ public void setPackageName(CharSequence packageName) { + enforceNotSealed(); mPackageName = packageName; } /** - * 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. * @@ -567,12 +478,33 @@ public final class AccessibilityEvent implements Parcelable { /** * Returns a cached instance if such is available or a new one is + * instantiated with type property set. + * + * @param event The other event. + * @return An instance. + */ + public static AccessibilityEvent obtain(AccessibilityEvent event) { + AccessibilityEvent eventClone = AccessibilityEvent.obtain(); + eventClone.init(event); + + final int recordCount = event.mRecords.size(); + for (int i = 0; i < recordCount; i++) { + AccessibilityRecord record = event.mRecords.get(i); + AccessibilityRecord recordClone = AccessibilityRecord.obtain(record); + eventClone.mRecords.add(recordClone); + } + + return eventClone; + } + + /** + * Returns a cached instance if such is available or a new one is * instantiated. * * @return An instance. */ public static AccessibilityEvent obtain() { - synchronized (mPoolLock) { + synchronized (sPoolLock) { if (sPool != null) { AccessibilityEvent event = sPool; sPool = sPool.mNext; @@ -589,14 +521,16 @@ public final class AccessibilityEvent implements Parcelable { * Return an instance back to be reused. * <p> * <b>Note: You must not touch the object after calling this function.</b> + * + * @throws IllegalStateException If the event is already recycled. */ + @Override public void recycle() { if (mIsInPool) { - return; + throw new IllegalStateException("Event already recycled!"); } - clear(); - synchronized (mPoolLock) { + synchronized (sPoolLock) { if (sPoolSize <= MAX_POOL_SIZE) { mNext = sPool; sPool = this; @@ -608,87 +542,123 @@ public final class AccessibilityEvent implements Parcelable { /** * Clears the state of this instance. + * + * @hide */ - private void clear() { + @Override + protected void clear() { + super.clear(); + mConnection = null; mEventType = 0; - mBooleanProperties = 0; - mCurrentItemIndex = INVALID_POSITION; - mItemCount = 0; - mFromIndex = 0; - mAddedCount = 0; - mRemovedCount = 0; - mEventTime = 0; - mClassName = null; + mSourceAccessibilityViewId = View.NO_ID; + mSourceAccessibilityWindowId = View.NO_ID; mPackageName = null; - mContentDescription = null; - mBeforeText = null; - mParcelableData = null; - mText.clear(); + mEventTime = 0; + while (!mRecords.isEmpty()) { + AccessibilityRecord record = mRecords.remove(0); + record.recycle(); + } } /** - * Gets the value of a boolean property. + * Creates a new instance from a {@link Parcel}. * - * @param property The property. - * @return The value. + * @param parcel A parcel containing the state of a {@link AccessibilityEvent}. */ - private boolean getBooleanProperty(int property) { - return (mBooleanProperties & property) == property; + public void initFromParcel(Parcel parcel) { + if (parcel.readInt() == 1) { + mConnection = IAccessibilityServiceConnection.Stub.asInterface( + parcel.readStrongBinder()); + } + setSealed(parcel.readInt() == 1); + mEventType = parcel.readInt(); + mSourceAccessibilityWindowId = parcel.readInt(); + mSourceAccessibilityViewId = parcel.readInt(); + mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + 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); + } } /** - * Sets a boolean property. + * Reads an {@link AccessibilityRecord} from a parcel. * - * @param property The property. - * @param value The value. + * @param record The record to initialize. + * @param parcel The parcel to read from. */ - private void setBooleanProperty(int property, boolean value) { - if (value) { - mBooleanProperties |= property; - } else { - mBooleanProperties &= ~property; - } + 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); } /** - * Creates a new instance from a {@link Parcel}. - * - * @param parcel A parcel containing the state of a {@link AccessibilityEvent}. + * {@inheritDoc} */ - 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); - } - public void writeToParcel(Parcel parcel, int flags) { + if (mConnection == null) { + parcel.writeInt(0); + } else { + parcel.writeInt(1); + parcel.writeStrongBinder(mConnection.asBinder()); + } + parcel.writeInt(isSealed() ? 1 : 0); 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); + parcel.writeInt(mSourceAccessibilityWindowId); + parcel.writeInt(mSourceAccessibilityViewId); 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,28 +666,79 @@ public final class AccessibilityEvent implements Parcelable { @Override public String toString() { StringBuilder builder = new StringBuilder(); + builder.append("; EventType: ").append(eventTypeToString(mEventType)); + builder.append("; EventTime: ").append(mEventTime); + builder.append("; PackageName: ").append(mPackageName); 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); + if (DEBUG) { + builder.append("\n"); + builder.append("; sourceAccessibilityWindowId: ").append(mSourceAccessibilityWindowId); + builder.append("; sourceAccessibilityViewId: ").append(mSourceAccessibilityViewId); + for (int i = 0; i < mRecords.size(); i++) { + AccessibilityRecord record = mRecords.get(i); + builder.append(" Record "); + builder.append(i); + builder.append(":"); + builder.append(" [ ClassName: " + record.mClassName); + builder.append("; Text: " + record.mText); + builder.append("; ContentDescription: " + record.mContentDescription); + builder.append("; ItemCount: " + record.mItemCount); + builder.append("; CurrentItemIndex: " + record.mCurrentItemIndex); + builder.append("; IsEnabled: " + record.isEnabled()); + builder.append("; IsPassword: " + record.isPassword()); + builder.append("; IsChecked: " + record.isChecked()); + builder.append("; IsFullScreen: " + record.isFullScreen()); + builder.append("; BeforeText: " + record.mBeforeText); + builder.append("; FromIndex: " + record.mFromIndex); + builder.append("; AddedCount: " + record.mAddedCount); + builder.append("; RemovedCount: " + record.mRemovedCount); + builder.append("; ParcelableData: " + record.mParcelableData); + builder.append(" ]"); + builder.append("\n"); + } + } else { + builder.append("; recordCount: ").append(getAddedCount()); + } return builder.toString(); } /** + * Returns the string representation of an event type. For example, + * {@link #TYPE_VIEW_CLICKED} is represented by the string TYPE_VIEW_CLICKED. + * + * @param feedbackType The event type + * @return The string representation. + */ + public static String eventTypeToString(int feedbackType) { + switch (feedbackType) { + case TYPE_VIEW_CLICKED: + return "TYPE_VIEW_CLICKED"; + case TYPE_VIEW_LONG_CLICKED: + return "TYPE_VIEW_LONG_CLICKED"; + case TYPE_VIEW_SELECTED: + return "TYPE_VIEW_SELECTED"; + case TYPE_VIEW_FOCUSED: + return "TYPE_VIEW_FOCUSED"; + case TYPE_VIEW_TEXT_CHANGED: + return "TYPE_VIEW_TEXT_CHANGED"; + case TYPE_WINDOW_STATE_CHANGED: + return "TYPE_WINDOW_STATE_CHANGED"; + case TYPE_VIEW_HOVER_ENTER: + return "TYPE_VIEW_HOVER_ENTER"; + case TYPE_VIEW_HOVER_EXIT: + return "TYPE_VIEW_HOVER_EXIT"; + case TYPE_NOTIFICATION_STATE_CHANGED: + return "TYPE_NOTIFICATION_STATE_CHANGED"; + case TYPE_TOUCH_EXPLORATION_GESTURE_START: + return "TYPE_TOUCH_EXPLORATION_GESTURE_START"; + case TYPE_TOUCH_EXPLORATION_GESTURE_END: + return "TYPE_TOUCH_EXPLORATION_GESTURE_END"; + default: + return null; + } + } + + /** * @see Parcelable.Creator */ public static final Parcelable.Creator<AccessibilityEvent> CREATOR = diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index f406da9..eece64a 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -16,8 +16,7 @@ package android.view.accessibility; -import static android.util.Config.LOGV; - +import android.accessibilityservice.AccessibilityServiceInfo; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; @@ -29,9 +28,13 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.util.Log; +import android.view.IWindow; +import android.view.View; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; /** * System level service that serves as an event dispatch for {@link AccessibilityEvent}s. @@ -46,6 +49,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(); @@ -60,6 +65,21 @@ public final class AccessibilityManager { boolean mIsEnabled; + final CopyOnWriteArrayList<AccessibilityStateChangeListener> mAccessibilityStateChangeListeners = + new CopyOnWriteArrayList<AccessibilityStateChangeListener>(); + + /** + * Listener for the accessibility state. + */ + public interface AccessibilityStateChangeListener { + /** + * Called back on change in the accessibility state. + * + * @param enabled + */ + public void onAccessibilityStateChanged(boolean enabled); + } + final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { public void setEnabled(boolean enabled) { mHandler.obtainMessage(DO_SET_ENABLED, enabled ? 1 : 0, 0).sendToTarget(); @@ -76,9 +96,8 @@ public final class AccessibilityManager { public void handleMessage(Message message) { switch (message.what) { case DO_SET_ENABLED : - synchronized (mHandler) { - mIsEnabled = (message.arg1 == 1); - } + final boolean isEnabled = (message.arg1 == 1); + setAccessibilityState(isEnabled); return; default : Log.w(LOG_TAG, "Unknown message type: " + message.what); @@ -115,7 +134,8 @@ public final class AccessibilityManager { mService = service; try { - mIsEnabled = mService.addClient(mClient); + final boolean isEnabled = mService.addClient(mClient); + setAccessibilityState(isEnabled); } catch (RemoteException re) { Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); } @@ -166,7 +186,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 +207,7 @@ public final class AccessibilityManager { } try { mService.interrupt(); - if (LOGV) { + if (DEBUG) { Log.i(LOG_TAG, "Requested interrupt from all services"); } } catch (RemoteException re) { @@ -199,12 +219,51 @@ public final class AccessibilityManager { * Returns the {@link ServiceInfo}s of the installed accessibility services. * * @return An unmodifiable list with {@link ServiceInfo}s. + * + * @deprecated Use {@link #getInstalledAccessibilityServiceList()} */ + @Deprecated public List<ServiceInfo> getAccessibilityServiceList() { - List<ServiceInfo> services = null; + List<AccessibilityServiceInfo> infos = getInstalledAccessibilityServiceList(); + List<ServiceInfo> services = new ArrayList<ServiceInfo>(); + final int infoCount = infos.size(); + for (int i = 0; i < infoCount; i++) { + AccessibilityServiceInfo info = infos.get(i); + services.add(info.getResolveInfo().serviceInfo); + } + return Collections.unmodifiableList(services); + } + + /** + * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. + * + * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. + */ + public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() { + List<AccessibilityServiceInfo> services = null; + try { + services = mService.getInstalledAccessibilityServiceList(); + 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 AccessibilityServiceInfo}s of the enabled accessibility services + * for a given feedback type. + * + * @param feedbackType The feedback type (can be bitwise or of multiple types). + * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. + */ + public List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackType) { + List<AccessibilityServiceInfo> services = null; try { - services = mService.getAccessibilityServiceList(); - if (LOGV) { + services = mService.getEnabledAccessibilityServiceList(feedbackType); + if (DEBUG) { Log.i(LOG_TAG, "Installed AccessibilityServices " + services); } } catch (RemoteException re) { @@ -212,4 +271,81 @@ public final class AccessibilityManager { } return Collections.unmodifiableList(services); } + + /** + * Registers an {@link AccessibilityStateChangeListener}. + * + * @param listener The listener. + * @return True if successfully registered. + */ + public boolean addAccessibilityStateChangeListener( + AccessibilityStateChangeListener listener) { + return mAccessibilityStateChangeListeners.add(listener); + } + + /** + * Unregisters an {@link AccessibilityStateChangeListener}. + * + * @param listener The listener. + * @return True if successfully unregistered. + */ + public boolean removeAccessibilityStateChangeListener( + AccessibilityStateChangeListener listener) { + return mAccessibilityStateChangeListeners.remove(listener); + } + + /** + * Sets the enabled state. + * + * @param isEnabled The accessibility state. + */ + private void setAccessibilityState(boolean isEnabled) { + synchronized (mHandler) { + if (isEnabled != mIsEnabled) { + mIsEnabled = isEnabled; + notifyAccessibilityStateChanged(); + } + } + } + + /** + * Notifies the registered {@link AccessibilityStateChangeListener}s. + */ + private void notifyAccessibilityStateChanged() { + final int listenerCount = mAccessibilityStateChangeListeners.size(); + for (int i = 0; i < listenerCount; i++) { + mAccessibilityStateChangeListeners.get(i).onAccessibilityStateChanged(mIsEnabled); + } + } + + /** + * Adds an accessibility interaction connection interface for a given window. + * @param windowToken The window token to which a connection is added. + * @param connection The connection. + * + * @hide + */ + public int addAccessibilityInteractionConnection(IWindow windowToken, + IAccessibilityInteractionConnection connection) { + try { + return mService.addAccessibilityInteractionConnection(windowToken, connection); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while adding an accessibility interaction connection. ", re); + } + return View.NO_ID; + } + + /** + * Removed an accessibility interaction connection interface for a given window. + * @param windowToken The window token to which a connection is removed. + * + * @hide + */ + public void removeAccessibilityInteractionConnection(IWindow windowToken) { + try { + mService.removeAccessibilityInteractionConnection(windowToken); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while removing an accessibility interaction connection. ", re); + } + } } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.aidl b/core/java/android/view/accessibility/AccessibilityNodeInfo.aidl new file mode 100644 index 0000000..59175ce --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.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.accessibility; + +parcelable AccessibilityNodeInfo; diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java new file mode 100644 index 0000000..5fa65b4 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -0,0 +1,969 @@ +/* + * 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.accessibilityservice.IAccessibilityServiceConnection; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.view.View; + +/** + * This class represents a node of the screen content. From the point of + * view of an accessibility service the screen content is presented as tree + * of accessibility nodes. + * + * TODO(svertoslavganov): Update the documentation, add sample, and describe + * the security policy. + */ +public class AccessibilityNodeInfo implements Parcelable { + + private static final boolean DEBUG = false; + + // Actions. + + /** + * Action that focuses the node. + */ + public static final int ACTION_FOCUS = 0x00000001; + + /** + * Action that unfocuses the node. + */ + public static final int ACTION_CLEAR_FOCUS = 0x00000002; + + /** + * Action that selects the node. + */ + public static final int ACTION_SELECT = 0x00000004; + + /** + * Action that unselects the node. + */ + public static final int ACTION_CLEAR_SELECTION = 0x00000008; + + // Boolean attributes. + + private static final int PROPERTY_CHECKABLE = 0x00000001; + + private static final int PROPERTY_CHECKED = 0x00000002; + + private static final int PROPERTY_FOCUSABLE = 0x00000004; + + private static final int PROPERTY_FOCUSED = 0x00000008; + + private static final int PROPERTY_SELECTED = 0x00000010; + + private static final int PROPERTY_CLICKABLE = 0x00000020; + + private static final int PROPERTY_LONG_CLICKABLE = 0x00000040; + + private static final int PROPERTY_ENABLED = 0x00000080; + + private static final int PROPERTY_PASSWORD = 0x00000100; + + // Readable representations - lazily initialized. + private static SparseArray<String> sActionSymbolicNames; + + // Housekeeping. + private static final int MAX_POOL_SIZE = 50; + private static final Object sPoolLock = new Object(); + private static AccessibilityNodeInfo sPool; + private static int sPoolSize; + private AccessibilityNodeInfo mNext; + private boolean mIsInPool; + private boolean mSealed; + + // Data. + private int mAccessibilityViewId = View.NO_ID; + private int mAccessibilityWindowId = View.NO_ID; + private int mParentAccessibilityViewId = View.NO_ID; + private int mBooleanProperties; + private final Rect mBounds = new Rect(); + + private CharSequence mPackageName; + private CharSequence mClassName; + private CharSequence mText; + private CharSequence mContentDescription; + + private final SparseIntArray mChildAccessibilityIds = new SparseIntArray(); + private int mActions; + + private IAccessibilityServiceConnection mConnection; + + /** + * Hide constructor from clients. + */ + private AccessibilityNodeInfo() { + /* do nothing */ + } + + /** + * Sets the source. + * + * @param source The info source. + */ + public void setSource(View source) { + enforceNotSealed(); + mAccessibilityViewId = source.getAccessibilityViewId(); + mAccessibilityWindowId = source.getAccessibilityWindowId(); + } + + /** + * Gets the id of the window from which the info comes from. + * + * @return The window id. + */ + public int getAccessibilityWindowId() { + return mAccessibilityWindowId; + } + + /** + * Gets the number of children. + * + * @return The child count. + */ + public int getChildCount() { + return mChildAccessibilityIds.size(); + } + + /** + * Get the child at given index. + * <p> + * <strong> + * It is a client responsibility to recycle the received info by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * @param index The child index. + * @return The child node. + * + * @throws IllegalStateException If called outside of an AccessibilityService. + * + */ + public AccessibilityNodeInfo getChild(int index) { + enforceSealed(); + final int childAccessibilityViewId = mChildAccessibilityIds.get(index); + try { + return mConnection.findAccessibilityNodeInfoByAccessibilityId(mAccessibilityWindowId, + childAccessibilityViewId); + } catch (RemoteException e) { + return null; + } + } + + /** + * Adds a child. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param child The child. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void addChild(View child) { + enforceNotSealed(); + final int childAccessibilityViewId = child.getAccessibilityViewId(); + final int index = mChildAccessibilityIds.size(); + mChildAccessibilityIds.put(index, childAccessibilityViewId); + } + + /** + * Gets the actions that can be performed on the node. + * + * @return The bit mask of with actions. + * + * @see AccessibilityNodeInfo#ACTION_FOCUS + * @see AccessibilityNodeInfo#ACTION_CLEAR_FOCUS + * @see AccessibilityNodeInfo#ACTION_SELECT + * @see AccessibilityNodeInfo#ACTION_CLEAR_SELECTION + */ + public int getActions() { + return mActions; + } + + /** + * Adds an action that can be performed on the node. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param action The action. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void addAction(int action) { + enforceNotSealed(); + mActions |= action; + } + + /** + * Performs an action on the node. + * <p> + * Note: An action can be performed only if the request is made + * from an {@link android.accessibilityservice.AccessibilityService}. + * </p> + * @param action The action to perform. + * @return True if the action was performed. + * + * @throws IllegalStateException If called outside of an AccessibilityService. + */ + public boolean performAction(int action) { + enforceSealed(); + try { + return mConnection.performAccessibilityAction(mAccessibilityWindowId, + mAccessibilityViewId, action); + } catch (RemoteException e) { + return false; + } + } + + /** + * Gets the unique id identifying this node's parent. + * <p> + * <strong> + * It is a client responsibility to recycle the received info by + * calling {@link AccessibilityNodeInfo#recycle()} to avoid creating + * of multiple instances. + * </strong> + * </p> + * @return The node's patent id. + */ + public AccessibilityNodeInfo getParent() { + enforceSealed(); + try { + return mConnection.findAccessibilityNodeInfoByAccessibilityId(mAccessibilityWindowId, + mParentAccessibilityViewId); + } catch (RemoteException e) { + return null; + } + } + + /** + * Sets the parent. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param parent The parent. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setParent(View parent) { + enforceNotSealed(); + mParentAccessibilityViewId = parent.getAccessibilityViewId(); + } + + /** + * Gets the node bounds in parent coordinates. + * + * @param outBounds The output node bounds. + */ + public void getBounds(Rect outBounds) { + outBounds.set(mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); + } + + /** + * Sets the node bounds in parent coordinates. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param bounds The node bounds. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setBounds(Rect bounds) { + enforceNotSealed(); + mBounds.set(bounds.left, bounds.top, bounds.right, bounds.bottom); + } + + /** + * Gets whether this node is checkable. + * + * @return True if the node is checkable. + */ + public boolean isCheckable() { + return getBooleanProperty(PROPERTY_CHECKABLE); + } + + /** + * Sets whether this node is checkable. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param checkable True if the node is checkable. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setCheckable(boolean checkable) { + setBooleanProperty(PROPERTY_CHECKABLE, checkable); + } + + /** + * Gets whether this node is checked. + * + * @return True if the node is checked. + */ + public boolean isChecked() { + return getBooleanProperty(PROPERTY_CHECKED); + } + + /** + * Sets whether this node is checked. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param checked True if the node is checked. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setChecked(boolean checked) { + setBooleanProperty(PROPERTY_CHECKED, checked); + } + + /** + * Gets whether this node is focusable. + * + * @return True if the node is focusable. + */ + public boolean isFocusable() { + return getBooleanProperty(PROPERTY_FOCUSABLE); + } + + /** + * Sets whether this node is focusable. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param focusable True if the node is focusable. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFocusable(boolean focusable) { + setBooleanProperty(PROPERTY_FOCUSABLE, focusable); + } + + /** + * Gets whether this node is focused. + * + * @return True if the node is focused. + */ + public boolean isFocused() { + return getBooleanProperty(PROPERTY_FOCUSED); + } + + /** + * Sets whether this node is focused. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param focused True if the node is focused. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFocused(boolean focused) { + setBooleanProperty(PROPERTY_FOCUSED, focused); + } + + /** + * Gets whether this node is selected. + * + * @return True if the node is selected. + */ + public boolean isSelected() { + return getBooleanProperty(PROPERTY_SELECTED); + } + + /** + * Sets whether this node is selected. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param selected True if the node is selected. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setSelected(boolean selected) { + setBooleanProperty(PROPERTY_SELECTED, selected); + } + + /** + * Gets whether this node is clickable. + * + * @return True if the node is clickable. + */ + public boolean isClickable() { + return getBooleanProperty(PROPERTY_CLICKABLE); + } + + /** + * Sets whether this node is clickable. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param clickable True if the node is clickable. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClickable(boolean clickable) { + setBooleanProperty(PROPERTY_CLICKABLE, clickable); + } + + /** + * Gets whether this node is long clickable. + * + * @return True if the node is long clickable. + */ + public boolean isLongClickable() { + return getBooleanProperty(PROPERTY_LONG_CLICKABLE); + } + + /** + * Sets whether this node is long clickable. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param longClickable True if the node is long clickable. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setLongClickable(boolean longClickable) { + setBooleanProperty(PROPERTY_LONG_CLICKABLE, longClickable); + } + + /** + * Gets whether this node is enabled. + * + * @return True if the node is enabled. + */ + public boolean isEnabled() { + return getBooleanProperty(PROPERTY_ENABLED); + } + + /** + * Sets whether this node is enabled. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param enabled True if the node is enabled. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setEnabled(boolean enabled) { + setBooleanProperty(PROPERTY_ENABLED, enabled); + } + + /** + * Gets whether this node is a password. + * + * @return True if the node is a password. + */ + public boolean isPassword() { + return getBooleanProperty(PROPERTY_PASSWORD); + } + + /** + * Sets whether this node is a password. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param password True if the node is a password. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPassword(boolean password) { + setBooleanProperty(PROPERTY_PASSWORD, password); + } + + /** + * Gets the package this node comes from. + * + * @return The package name. + */ + public CharSequence getPackageName() { + return mPackageName; + } + + /** + * Sets the package this node comes from. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param packageName The package name. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPackageName(CharSequence packageName) { + enforceNotSealed(); + mPackageName = packageName; + } + + /** + * Gets the class this node comes from. + * + * @return The class name. + */ + public CharSequence getClassName() { + return mClassName; + } + + /** + * Sets the class this node comes from. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param className The class name. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClassName(CharSequence className) { + enforceNotSealed(); + mClassName = className; + } + + /** + * Gets the text of this node. + * + * @return The text. + */ + public CharSequence getText() { + return mText; + } + + /** + * Sets the text of this node. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param text The text. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setText(CharSequence text) { + enforceNotSealed(); + mText = text; + } + + /** + * Gets the content description of this node. + * + * @return The content description. + */ + public CharSequence getContentDescription() { + return mContentDescription; + } + + /** + * Sets the content description of this node. + * <p> + * Note: Cannot be called from an {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * </p> + * @param contentDescription The content description. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setContentDescription(CharSequence contentDescription) { + enforceNotSealed(); + mContentDescription = contentDescription; + } + + /** + * Gets the value of a boolean property. + * + * @param property The property. + * @return The value. + */ + private boolean getBooleanProperty(int property) { + return (mBooleanProperties & property) != 0; + } + + /** + * Sets a boolean property. + * + * @param property The property. + * @param value The value. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + private void setBooleanProperty(int property, boolean value) { + enforceNotSealed(); + if (value) { + mBooleanProperties |= property; + } else { + mBooleanProperties &= ~property; + } + } + + /** + * Sets the connection for interacting with the system. + * + * @param connection The client token. + * + * @hide + */ + public final void setConnection(IAccessibilityServiceConnection connection) { + mConnection = connection; + } + + /** + * {@inheritDoc} + */ + public int describeContents() { + return 0; + } + + /** + * Sets if this instance is sealed. + * + * @param sealed Whether is sealed. + * + * @hide + */ + public void setSealed(boolean sealed) { + mSealed = sealed; + } + + /** + * Gets if this instance is sealed. + * + * @return Whether is sealed. + * + * @hide + */ + public boolean isSealed() { + return mSealed; + } + + /** + * Enforces that this instance is sealed. + * + * @throws IllegalStateException If this instance is not sealed. + * + * @hide + */ + protected void enforceSealed() { + if (!isSealed()) { + throw new IllegalStateException("Cannot perform this " + + "action on a not sealed instance."); + } + } + + /** + * Enforces that this instance is not sealed. + * + * @throws IllegalStateException If this instance is sealed. + * + * @hide + */ + protected void enforceNotSealed() { + if (isSealed()) { + throw new IllegalStateException("Cannot perform this " + + "action on an sealed instance."); + } + } + + /** + * Returns a cached instance if such is available otherwise a new one + * and sets the source. + * + * @return An instance. + * + * @see #setSource(View) + */ + public static AccessibilityNodeInfo obtain(View source) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.setSource(source); + return info; + } + + /** + * Returns a cached instance if such is available otherwise a new one. + * + * @return An instance. + */ + public static AccessibilityNodeInfo obtain() { + synchronized (sPoolLock) { + if (sPool != null) { + AccessibilityNodeInfo info = sPool; + sPool = sPool.mNext; + sPoolSize--; + info.mNext = null; + info.mIsInPool = false; + return info; + } + return new AccessibilityNodeInfo(); + } + } + + /** + * Return an instance back to be reused. + * <p> + * <b>Note: You must not touch the object after calling this function.</b> + * + * @throws IllegalStateException If the info is already recycled. + */ + public void recycle() { + if (mIsInPool) { + throw new IllegalStateException("Info already recycled!"); + } + clear(); + synchronized (sPoolLock) { + if (sPoolSize <= MAX_POOL_SIZE) { + mNext = sPool; + sPool = this; + mIsInPool = true; + sPoolSize++; + } + } + } + + /** + * {@inheritDoc} + * <p> + * <b>Note: After the instance is written to a parcel it is recycled. + * You must not touch the object after calling this function.</b> + * </p> + */ + public void writeToParcel(Parcel parcel, int flags) { + if (mConnection == null) { + parcel.writeInt(0); + } else { + parcel.writeInt(1); + parcel.writeStrongBinder(mConnection.asBinder()); + } + parcel.writeInt(isSealed() ? 1 : 0); + parcel.writeInt(mAccessibilityViewId); + parcel.writeInt(mAccessibilityWindowId); + parcel.writeInt(mParentAccessibilityViewId); + + SparseIntArray childIds = mChildAccessibilityIds; + final int childIdsSize = childIds.size(); + parcel.writeInt(childIdsSize); + for (int i = 0; i < childIdsSize; i++) { + parcel.writeInt(childIds.valueAt(i)); + } + + parcel.writeInt(mBounds.top); + parcel.writeInt(mBounds.bottom); + parcel.writeInt(mBounds.left); + parcel.writeInt(mBounds.right); + + parcel.writeInt(mActions); + + parcel.writeInt(mBooleanProperties); + + TextUtils.writeToParcel(mPackageName, parcel, flags); + TextUtils.writeToParcel(mClassName, parcel, flags); + TextUtils.writeToParcel(mText, parcel, flags); + TextUtils.writeToParcel(mContentDescription, parcel, flags); + + // Since instances of this class are fetched via synchronous i.e. blocking + // calls in IPCs and we always recycle as soon as the instance is marshaled. + recycle(); + } + + /** + * Creates a new instance from a {@link Parcel}. + * + * @param parcel A parcel containing the state of a {@link AccessibilityNodeInfo}. + */ + private void initFromParcel(Parcel parcel) { + if (parcel.readInt() == 1) { + mConnection = IAccessibilityServiceConnection.Stub.asInterface( + parcel.readStrongBinder()); + } + mSealed = (parcel.readInt() == 1); + mAccessibilityViewId = parcel.readInt(); + mAccessibilityWindowId = parcel.readInt(); + mParentAccessibilityViewId = parcel.readInt(); + + SparseIntArray childIds = mChildAccessibilityIds; + final int childrenSize = parcel.readInt(); + for (int i = 0; i < childrenSize; i++) { + final int childId = parcel.readInt(); + childIds.put(i, childId); + } + + mBounds.top = parcel.readInt(); + mBounds.bottom = parcel.readInt(); + mBounds.left = parcel.readInt(); + mBounds.right = parcel.readInt(); + + mActions = parcel.readInt(); + + mBooleanProperties = parcel.readInt(); + + mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + mClassName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + mText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); + } + + /** + * Clears the state of this instance. + */ + private void clear() { + mSealed = false; + mConnection = null; + mAccessibilityViewId = View.NO_ID; + mParentAccessibilityViewId = View.NO_ID; + mChildAccessibilityIds.clear(); + mBounds.set(0, 0, 0, 0); + mBooleanProperties = 0; + mPackageName = null; + mClassName = null; + mText = null; + mContentDescription = null; + mActions = 0; + } + + /** + * Gets the human readable action symbolic name. + * + * @param action The action. + * @return The symbolic name. + */ + private static String getActionSymbolicName(int action) { + SparseArray<String> actionSymbolicNames = sActionSymbolicNames; + if (actionSymbolicNames == null) { + actionSymbolicNames = sActionSymbolicNames = new SparseArray<String>(); + actionSymbolicNames.put(ACTION_FOCUS, "ACTION_FOCUS"); + actionSymbolicNames.put(ACTION_CLEAR_FOCUS, "ACTION_UNFOCUS"); + actionSymbolicNames.put(ACTION_SELECT, "ACTION_SELECT"); + actionSymbolicNames.put(ACTION_CLEAR_SELECTION, "ACTION_UNSELECT"); + } + return actionSymbolicNames.get(action); + } + + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object == null) { + return false; + } + if (getClass() != object.getClass()) { + return false; + } + AccessibilityNodeInfo other = (AccessibilityNodeInfo) object; + if (mAccessibilityViewId != other.mAccessibilityViewId) { + return false; + } + if (mAccessibilityWindowId != other.mAccessibilityWindowId) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mAccessibilityViewId; + result = prime * result + mAccessibilityWindowId; + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(super.toString()); + + if (DEBUG) { + builder.append("; accessibilityId: " + mAccessibilityViewId); + builder.append("; parentAccessibilityId: " + mParentAccessibilityViewId); + SparseIntArray childIds = mChildAccessibilityIds; + builder.append("; childAccessibilityIds: ["); + for (int i = 0, count = childIds.size(); i < count; i++) { + builder.append(childIds.valueAt(i)); + if (i < count - 1) { + builder.append(", "); + } + } + builder.append("]"); + } + + builder.append("; bounds: " + mBounds); + + builder.append("; packageName: ").append(mPackageName); + builder.append("; className: ").append(mClassName); + builder.append("; text: ").append(mText); + builder.append("; contentDescription: ").append(mContentDescription); + + builder.append("; checkable: ").append(isCheckable()); + builder.append("; checked: ").append(isChecked()); + builder.append("; focusable: ").append(isFocusable()); + builder.append("; focused: ").append(isFocused()); + builder.append("; selected: ").append(isSelected()); + builder.append("; clickable: ").append(isClickable()); + builder.append("; longClickable: ").append(isLongClickable()); + builder.append("; enabled: ").append(isEnabled()); + builder.append("; password: ").append(isPassword()); + + builder.append("; ["); + + for (int actionBits = mActions; actionBits != 0;) { + final int action = 1 << Integer.numberOfTrailingZeros(actionBits); + actionBits &= ~action; + builder.append(getActionSymbolicName(action)); + if (actionBits != 0) { + builder.append(", "); + } + } + + builder.append("]"); + + return builder.toString(); + } + + /** + * @see Parcelable.Creator + */ + public static final Parcelable.Creator<AccessibilityNodeInfo> CREATOR = + new Parcelable.Creator<AccessibilityNodeInfo>() { + public AccessibilityNodeInfo createFromParcel(Parcel parcel) { + AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); + info.initFromParcel(parcel); + return info; + } + + public AccessibilityNodeInfo[] newArray(int size) { + return new AccessibilityNodeInfo[size]; + } + }; +} diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java new file mode 100644 index 0000000..4bf03a7 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -0,0 +1,543 @@ +/* + * 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; + + // Housekeeping + private static final int MAX_POOL_SIZE = 10; + private static final Object sPoolLock = new Object(); + private static AccessibilityRecord sPool; + private static int sPoolSize; + private AccessibilityRecord mNext; + private boolean mIsInPool; + private boolean mSealed; + + 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() { + + } + + /** + * Initialize this record from another one. + * + * @param record The to initialize from. + */ + void init(AccessibilityRecord record) { + mSealed = record.isSealed(); + mBooleanProperties = record.mBooleanProperties; + mCurrentItemIndex = record.mCurrentItemIndex; + mItemCount = record.mItemCount; + mFromIndex = record.mFromIndex; + mAddedCount = record.mAddedCount; + mRemovedCount = record.mRemovedCount; + mClassName = record.mClassName; + mContentDescription = record.mContentDescription; + mBeforeText = record.mBeforeText; + mParcelableData = record.mParcelableData; + mText.addAll(record.mText); + } + + /** + * 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setChecked(boolean isChecked) { + enforceNotSealed(); + 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setEnabled(boolean isEnabled) { + enforceNotSealed(); + 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPassword(boolean isPassword) { + enforceNotSealed(); + setBooleanProperty(PROPERTY_PASSWORD, isPassword); + } + + /** + * 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); + } + + /** + * Sets if the source is taking the entire screen. + * + * @param isFullScreen True if the source is full screen, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFullScreen(boolean isFullScreen) { + enforceNotSealed(); + setBooleanProperty(PROPERTY_FULL_SCREEN, isFullScreen); + } + + /** + * 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setItemCount(int itemCount) { + enforceNotSealed(); + 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setCurrentItemIndex(int currentItemIndex) { + enforceNotSealed(); + 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFromIndex(int fromIndex) { + enforceNotSealed(); + 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setAddedCount(int addedCount) { + enforceNotSealed(); + 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setRemovedCount(int removedCount) { + enforceNotSealed(); + 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClassName(CharSequence className) { + enforceNotSealed(); + 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setBeforeText(CharSequence beforeText) { + enforceNotSealed(); + 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setContentDescription(CharSequence contentDescription) { + enforceNotSealed(); + 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. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setParcelableData(Parcelable parcelableData) { + enforceNotSealed(); + mParcelableData = parcelableData; + } + + /** + * Sets if this instance is sealed. + * + * @param sealed Whether is sealed. + * + * @hide + */ + public void setSealed(boolean sealed) { + mSealed = sealed; + } + + /** + * Gets if this instance is sealed. + * + * @return Whether is sealed. + * + * @hide + */ + public boolean isSealed() { + return mSealed; + } + + /** + * Enforces that this instance is sealed. + * + * @throws IllegalStateException If this instance is not sealed. + * + * @hide + */ + protected void enforceSealed() { + if (!isSealed()) { + throw new IllegalStateException("Cannot perform this " + + "action on a not sealed instance."); + } + } + + /** + * Enforces that this instance is not sealed. + * + * @throws IllegalStateException If this instance is sealed. + * + * @hide + */ + protected void enforceNotSealed() { + if (isSealed()) { + throw new IllegalStateException("Cannot perform this " + + "action on an sealed instance."); + } + } + + /** + * 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; + } + } + + /** + * Returns a cached instance if such is available or a new one is + * instantiated. The instance is initialized with data from the + * given record. + * + * @return An instance. + */ + public static AccessibilityRecord obtain(AccessibilityRecord record) { + AccessibilityRecord clone = AccessibilityRecord.obtain(); + clone.init(record); + return clone; + } + + /** + * Returns a cached instance if such is available or a new one is + * instantiated. + * + * @return An instance. + */ + public static AccessibilityRecord obtain() { + synchronized (sPoolLock) { + if (sPool != null) { + AccessibilityRecord record = sPool; + sPool = sPool.mNext; + sPoolSize--; + record.mNext = null; + record.mIsInPool = false; + return record; + } + return new AccessibilityRecord(); + } + } + + /** + * Return an instance back to be reused. + * <p> + * <b>Note: You must not touch the object after calling this function.</b> + * + * @throws IllegalStateException If the record is already recycled. + */ + public void recycle() { + if (mIsInPool) { + throw new IllegalStateException("Record already recycled!"); + } + clear(); + synchronized (sPoolLock) { + if (sPoolSize <= MAX_POOL_SIZE) { + mNext = sPool; + sPool = this; + mIsInPool = true; + sPoolSize++; + } + } + } + + /** + * Clears the state of this instance. + * + * @hide + */ + protected void clear() { + mSealed = false; + 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/IAccessibilityInteractionConnection.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl new file mode 100644 index 0000000..77dcd07 --- /dev/null +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl @@ -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.view.accessibility; + +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityInteractionConnectionCallback; + +/** + * Interface for interaction between the AccessibilityManagerService + * and the ViewRoot in a given window. + * + * @hide + */ +oneway interface IAccessibilityInteractionConnection { + + void findAccessibilityNodeInfoByAccessibilityId(int accessibilityViewId, int interactionId, + IAccessibilityInteractionConnectionCallback callback); + + void findAccessibilityNodeInfoByViewId(int id, int interactionId, + IAccessibilityInteractionConnectionCallback callback); + + void findAccessibilityNodeInfosByViewText(String text, int interactionId, + IAccessibilityInteractionConnectionCallback callback); + + void performAccessibilityAction(int accessibilityId, int action, int interactionId, + IAccessibilityInteractionConnectionCallback callback); +} diff --git a/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl new file mode 100644 index 0000000..9c5e8dc --- /dev/null +++ b/core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl @@ -0,0 +1,36 @@ +/* + * 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.view.accessibility.AccessibilityNodeInfo; +import java.util.List; + +/** + * Callback for specifying the result for an asynchronous request made + * via calling a method on IAccessibilityInteractionConnectionCallback. + * + * @hide + */ +oneway interface IAccessibilityInteractionConnectionCallback { + + void setFindAccessibilityNodeInfoResult(in AccessibilityNodeInfo info, int interactionId); + + void setFindAccessibilityNodeInfosResult(in List<AccessibilityNodeInfo> infos, + int interactionId); + + void setPerformAccessibilityActionResult(boolean succeeded, int interactionId); +} diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 7633569..b14f02a 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -17,9 +17,14 @@ package android.view.accessibility; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.accessibilityservice.IAccessibilityServiceConnection; +import android.accessibilityservice.IEventListener; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; import android.view.accessibility.IAccessibilityManagerClient; -import android.content.pm.ServiceInfo; +import android.view.IWindow; /** * Interface implemented by the AccessibilityManagerService called by @@ -33,7 +38,16 @@ interface IAccessibilityManager { boolean sendAccessibilityEvent(in AccessibilityEvent uiEvent); - List<ServiceInfo> getAccessibilityServiceList(); + List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList(); + + List<AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int feedbackType); void interrupt(); + + int addAccessibilityInteractionConnection(IWindow windowToken, + in IAccessibilityInteractionConnection connection); + + void removeAccessibilityInteractionConnection(IWindow windowToken); + + IAccessibilityServiceConnection registerEventListener(IEventListener client); } diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index e644045..abe3c2c 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -34,7 +34,7 @@ import android.util.LogPrinter; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; -import android.view.ViewRoot; +import android.view.ViewAncestor; class ComposingText implements NoCopySpan { } @@ -49,8 +49,9 @@ public class BaseInputConnection implements InputConnection { private static final boolean DEBUG = false; private static final String TAG = "BaseInputConnection"; static final Object COMPOSING = new ComposingText(); - - final InputMethodManager mIMM; + + /** @hide */ + protected final InputMethodManager mIMM; final View mTargetView; final boolean mDummyMode; @@ -501,7 +502,7 @@ public class BaseInputConnection implements InputConnection { } } if (h != null) { - h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, event)); } } @@ -644,7 +645,7 @@ public class BaseInputConnection implements InputConnection { lp.println("Composing text:"); TextUtils.dumpSpans(text, lp, " "); } - + // Position the cursor appropriately, so that after replacing the // desired range of text it will be located in the correct spot. // This allows us to deal with filters performing edits on the text diff --git a/core/java/android/view/inputmethod/InputConnectionWrapper.java b/core/java/android/view/inputmethod/InputConnectionWrapper.java index 4d9d51e..690ea85 100644 --- a/core/java/android/view/inputmethod/InputConnectionWrapper.java +++ b/core/java/android/view/inputmethod/InputConnectionWrapper.java @@ -58,8 +58,7 @@ public class InputConnectionWrapper implements InputConnection { return mTarget.getCursorCapsMode(reqModes); } - public ExtractedText getExtractedText(ExtractedTextRequest request, - int flags) { + public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { return mTarget.getExtractedText(request, flags); } diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java index 32eec9f..4ec4ff9 100644 --- a/core/java/android/view/inputmethod/InputMethodInfo.java +++ b/core/java/android/view/inputmethod/InputMethodInfo.java @@ -23,9 +23,9 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; @@ -38,6 +38,8 @@ import android.util.Xml; import java.io.IOException; import java.util.ArrayList; +import java.util.List; +import java.util.Map; /** * This class is used to specify meta information of an input method. @@ -77,13 +79,28 @@ public final class InputMethodInfo implements Parcelable { /** * Constructor. - * + * * @param context The Context in which we are parsing the input method. * @param service The ResolveInfo returned from the package manager about * this input method's component. */ public InputMethodInfo(Context context, ResolveInfo service) throws XmlPullParserException, IOException { + this(context, service, null); + } + + /** + * Constructor. + * + * @param context The Context in which we are parsing the input method. + * @param service The ResolveInfo returned from the package manager about + * this input method's component. + * @param additionalSubtypes additional subtypes being added to this InputMethodInfo + * @hide + */ + public InputMethodInfo(Context context, ResolveInfo service, + Map<String, List<InputMethodSubtype>> additionalSubtypesMap) + throws XmlPullParserException, IOException { mService = service; ServiceInfo si = service.serviceInfo; mId = new ComponentName(si.packageName, si.name).flattenToShortString(); @@ -145,7 +162,9 @@ public final class InputMethodInfo implements Parcelable { a.getString(com.android.internal.R.styleable .InputMethod_Subtype_imeSubtypeMode), a.getString(com.android.internal.R.styleable - .InputMethod_Subtype_imeSubtypeExtraValue)); + .InputMethod_Subtype_imeSubtypeExtraValue), + a.getBoolean(com.android.internal.R.styleable + .InputMethod_Subtype_isAuxiliary, false)); mSubtypes.add(subtype); } } @@ -155,6 +174,17 @@ public final class InputMethodInfo implements Parcelable { } finally { if (parser != null) parser.close(); } + + if (additionalSubtypesMap != null && additionalSubtypesMap.containsKey(mId)) { + final List<InputMethodSubtype> additionalSubtypes = additionalSubtypesMap.get(mId); + final int N = additionalSubtypes.size(); + for (int i = 0; i < N; ++i) { + final InputMethodSubtype subtype = additionalSubtypes.get(i); + if (!mSubtypes.contains(subtype)) { + mSubtypes.add(subtype); + } + } + } mSettingsActivityName = settingsActivityComponent; mIsDefaultResId = isDefaultResId; } @@ -322,7 +352,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 83d2a79..47f5e4c 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -35,13 +35,14 @@ import android.os.Message; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; +import android.text.style.SuggestionSpan; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; -import android.view.ViewRoot; +import android.view.ViewAncestor; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -505,6 +506,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 { @@ -543,7 +551,25 @@ public final class InputMethodManager { public void setFullscreenMode(boolean fullScreen) { mFullscreenMode = fullScreen; } - + + /** @hide */ + public void registerSuggestionSpansForNotification(SuggestionSpan[] spans) { + try { + mService.registerSuggestionSpansForNotification(spans); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** @hide */ + public void notifySuggestionPicked(SuggestionSpan span, String originalString, int index) { + try { + mService.notifySuggestionPicked(span, originalString, index); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + /** * Allows you to discover whether the attached input method is running * in fullscreen mode. Return true if it is fullscreen, entirely covering @@ -629,7 +655,7 @@ public final class InputMethodManager { if (vh != null) { // This will result in a call to reportFinishInputConnection() // below. - vh.sendMessage(vh.obtainMessage(ViewRoot.FINISH_INPUT_CONNECTION, + vh.sendMessage(vh.obtainMessage(ViewAncestor.FINISH_INPUT_CONNECTION, mServedInputConnection)); } } @@ -1086,9 +1112,9 @@ public final class InputMethodManager { void scheduleCheckFocusLocked(View view) { Handler vh = view.getHandler(); - if (vh != null && !vh.hasMessages(ViewRoot.CHECK_FOCUS)) { + if (vh != null && !vh.hasMessages(ViewAncestor.CHECK_FOCUS)) { // This will result in a call to checkFocus() below. - vh.sendMessage(vh.obtainMessage(ViewRoot.CHECK_FOCUS)); + vh.sendMessage(vh.obtainMessage(ViewAncestor.CHECK_FOCUS)); } } @@ -1143,7 +1169,7 @@ public final class InputMethodManager { } /** - * Called by ViewRoot when its window gets input focus. + * Called by ViewAncestor when its window gets input focus. * @hide */ public void onWindowFocus(View rootView, View focusedView, int softInputMode, @@ -1429,16 +1455,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 +1486,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 +1503,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 +1538,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 +1558,35 @@ public final class InputMethodManager { } } + /** + * Set additional input method subtypes. + * @param imeToken Supplies the identifying token given to an input method. + * @param subtypes subtypes will be added as additional subtypes of the current input method. + * @return true if the additional input method subtypes are successfully added. + */ + public boolean setAdditionalInputMethodSubtypes( + IBinder imeToken, InputMethodSubtype[] subtypes) { + synchronized (mH) { + try { + return mService.setAdditionalInputMethodSubtypes(imeToken, subtypes); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + return false; + } + } + } + + public InputMethodSubtype getLastInputMethodSubtype() { + synchronized (mH) { + try { + return mService.getLastInputMethodSubtype(); + } catch (RemoteException e) { + Log.w(TAG, "IME died: " + mCurId, e); + return null; + } + } + } + void doDump(FileDescriptor fd, PrintWriter fout, String[] args) { final Printer p = new PrintWriterPrinter(fout); p.println("Input method client state for " + this + ":"); diff --git a/core/java/android/view/inputmethod/InputMethodSubtype.java b/core/java/android/view/inputmethod/InputMethodSubtype.java index 807f6ce..9d84c3e 100644 --- a/core/java/android/view/inputmethod/InputMethodSubtype.java +++ b/core/java/android/view/inputmethod/InputMethodSubtype.java @@ -17,8 +17,10 @@ package android.view.inputmethod; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.os.Parcel; import android.os.Parcelable; +import android.text.TextUtils; import android.util.Slog; import java.util.ArrayList; @@ -26,6 +28,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; /** * This class is used to specify meta information of a subtype contained in an input method. @@ -38,12 +41,13 @@ public final class InputMethodSubtype implements Parcelable { private static final String EXTRA_VALUE_PAIR_SEPARATOR = ","; private static final String EXTRA_VALUE_KEY_VALUE_SEPARATOR = "="; - private final int mSubtypeNameResId; + private final boolean mIsAuxiliary; + private final int mSubtypeHashCode; private final int mSubtypeIconResId; + private final int mSubtypeNameResId; private final String mSubtypeLocale; private final String mSubtypeMode; private final String mSubtypeExtraValue; - private final int mSubtypeHashCode; private HashMap<String, String> mExtraValueHashMapCache; /** @@ -51,16 +55,33 @@ public final class InputMethodSubtype implements Parcelable { * @param nameId The name of the subtype * @param iconId The icon of the subtype * @param locale The locale supported by the subtype - * @param modeId The mode supported by the subtype + * @param mode The mode supported by the subtype + * @param extraValue The extra value of the subtype + */ + public InputMethodSubtype( + int nameId, int iconId, String locale, String mode, String extraValue) { + this(nameId, iconId, locale, mode, extraValue, false); + } + + /** + * Constructor + * @param nameId The name of the subtype + * @param iconId The icon of the subtype + * @param locale The locale supported by the subtype + * @param mode The mode supported by the subtype * @param extraValue The extra value of the subtype + * @param isAuxiliary true when this subtype is one shot subtype. */ - InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue) { + public InputMethodSubtype(int nameId, int iconId, String locale, String mode, String extraValue, + boolean isAuxiliary) { mSubtypeNameResId = nameId; mSubtypeIconResId = iconId; mSubtypeLocale = locale != null ? locale : ""; mSubtypeMode = mode != null ? mode : ""; mSubtypeExtraValue = extraValue != null ? extraValue : ""; - mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue); + mIsAuxiliary = isAuxiliary; + mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue, + mIsAuxiliary); } InputMethodSubtype(Parcel source) { @@ -73,7 +94,9 @@ public final class InputMethodSubtype implements Parcelable { mSubtypeMode = s != null ? s : ""; s = source.readString(); mSubtypeExtraValue = s != null ? s : ""; - mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue); + mIsAuxiliary = (source.readInt() == 1); + mSubtypeHashCode = hashCodeInternal(mSubtypeLocale, mSubtypeMode, mSubtypeExtraValue, + mIsAuxiliary); } /** @@ -111,6 +134,41 @@ public final class InputMethodSubtype implements Parcelable { return mSubtypeExtraValue; } + /** + * @return true if this subtype is one shot subtype. One shot subtype will not be shown in the + * ime switch list when this subtype is implicitly enabled. The framework will never + * switch the current ime to this subtype by switchToLastInputMethod in InputMethodManager. + */ + public boolean isAuxiliary() { + return mIsAuxiliary; + } + + /** + * @param context Context will be used for getting Locale and PackageManager. + * @param packageName The package name of the IME + * @param appInfo The application info of the IME + * @return a display name for this subtype. The string resource of the label (mSubtypeNameResId) + * can have only one %s in it. If there is, the %s part will be replaced with the locale's + * display name by the formatter. If there is not, this method simply returns the string + * specified by mSubtypeNameResId. If mSubtypeNameResId is not specified (== 0), it's up to the + * framework to generate an appropriate display name. + */ + public CharSequence getDisplayName( + Context context, String packageName, ApplicationInfo appInfo) { + final Locale locale = constructLocaleFromString(mSubtypeLocale); + final String localeStr = locale != null ? locale.getDisplayName() : mSubtypeLocale; + if (mSubtypeNameResId == 0) { + return localeStr; + } + final String subtypeName = context.getPackageManager().getText( + packageName, mSubtypeNameResId, appInfo).toString(); + if (!TextUtils.isEmpty(subtypeName)) { + return String.format(subtypeName, localeStr); + } else { + return localeStr; + } + } + private HashMap<String, String> getExtraValueHashMap() { if (mExtraValueHashMapCache == null) { mExtraValueHashMapCache = new HashMap<String, String>(); @@ -165,36 +223,59 @@ public final class InputMethodSubtype implements Parcelable { && (subtype.getMode().equals(getMode())) && (subtype.getIconResId() == getIconResId()) && (subtype.getLocale().equals(getLocale())) - && (subtype.getExtraValue().equals(getExtraValue())); + && (subtype.getExtraValue().equals(getExtraValue())) + && (subtype.isAuxiliary() == isAuxiliary()); } return false; } + @Override public int describeContents() { return 0; } + @Override public void writeToParcel(Parcel dest, int parcelableFlags) { dest.writeInt(mSubtypeNameResId); dest.writeInt(mSubtypeIconResId); dest.writeString(mSubtypeLocale); dest.writeString(mSubtypeMode); dest.writeString(mSubtypeExtraValue); + dest.writeInt(mIsAuxiliary ? 1 : 0); } public static final Parcelable.Creator<InputMethodSubtype> CREATOR = new Parcelable.Creator<InputMethodSubtype>() { + @Override public InputMethodSubtype createFromParcel(Parcel source) { return new InputMethodSubtype(source); } + @Override public InputMethodSubtype[] newArray(int size) { return new InputMethodSubtype[size]; } }; - private static int hashCodeInternal(String locale, String mode, String extraValue) { - return Arrays.hashCode(new Object[] {locale, mode, extraValue}); + private static Locale constructLocaleFromString(String localeStr) { + if (TextUtils.isEmpty(localeStr)) + return null; + String[] localeParams = localeStr.split("_", 3); + // The length of localeStr is guaranteed to always return a 1 <= value <= 3 + // because localeStr is not empty. + if (localeParams.length == 1) { + return new Locale(localeParams[0]); + } else if (localeParams.length == 2) { + return new Locale(localeParams[0], localeParams[1]); + } else if (localeParams.length == 3) { + return new Locale(localeParams[0], localeParams[1], localeParams[2]); + } + return null; + } + + private static int hashCodeInternal(String locale, String mode, String extraValue, + boolean isAuxiliary) { + return Arrays.hashCode(new Object[] {locale, mode, extraValue, isAuxiliary}); } /** diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index c7a7374..2f4774f 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -35,7 +35,7 @@ import android.os.Message; import android.util.Log; import android.util.TypedValue; import android.view.Surface; -import android.view.ViewRoot; +import android.view.ViewAncestor; import android.view.WindowManager; import junit.framework.Assert; @@ -44,6 +44,9 @@ import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; import java.net.URLEncoder; +import java.nio.charset.Charsets; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; @@ -222,7 +225,7 @@ class BrowserFrame extends Handler { sConfigCallback = new ConfigCallback( (WindowManager) appContext.getSystemService( Context.WINDOW_SERVICE)); - ViewRoot.addConfigCallback(sConfigCallback); + ViewAncestor.addConfigCallback(sConfigCallback); } sConfigCallback.addHandler(this); @@ -1043,13 +1046,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 +1072,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 +1092,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('/'); @@ -1138,7 +1144,7 @@ class BrowserFrame extends Handler { } /** - * Called by JNI when the native HTTP(S) stack gets an invalid cert chain. + * Called by JNI when the native HTTPS stack gets an invalid cert chain. * * We delegate the request to CallbackProxy, and route its response to * {@link #nativeSslCertErrorProceed(int)} or @@ -1179,6 +1185,32 @@ class BrowserFrame extends Handler { } /** + * Called by JNI when the native HTTPS stack gets a client + * certificate request. + * + * We delegate the request to CallbackProxy, and route its response to + * {@link #nativeSslClientCert(int, X509Certificate)}. + */ + private void requestClientCert(int handle, byte[] host_and_port_bytes) { + String host_and_port = new String(host_and_port_bytes, Charsets.UTF_8); + SslClientCertLookupTable table = SslClientCertLookupTable.getInstance(); + if (table.IsAllowed(host_and_port)) { + // previously allowed + nativeSslClientCert(handle, + table.PrivateKey(host_and_port), + table.CertificateChain(host_and_port)); + } else if (table.IsDenied(host_and_port)) { + // previously denied + nativeSslClientCert(handle, null, null); + } else { + // previously ignored or new + mCallbackProxy.onReceivedClientCertRequest( + new ClientCertRequestHandler(this, handle, host_and_port, table), + host_and_port); + } + } + + /** * Called by JNI when the native HTTP stack needs to download a file. * * We delegate the request to CallbackProxy, which owns the current app's @@ -1363,4 +1395,8 @@ class BrowserFrame extends Handler { private native void nativeSslCertErrorProceed(int handle); private native void nativeSslCertErrorCancel(int handle, int cert_error); + + native void nativeSslClientCert(int handle, + byte[] pkcs8EncodedPrivateKey, + byte[][] asn1DerEncodedCertificateChain); } diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 23fd12d..f7d55f6 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -118,6 +118,7 @@ class CallbackProxy extends Handler { private static final int SET_INSTALLABLE_WEBAPP = 138; private static final int NOTIFY_SEARCHBOX_LISTENERS = 139; private static final int AUTO_LOGIN = 140; + private static final int CLIENT_CERT_REQUEST = 141; // Message triggered by the client to resume execution private static final int NOTIFY = 200; @@ -273,7 +274,7 @@ class CallbackProxy extends Handler { mWebViewClient.onPageFinished(mWebView, finishedUrl); } break; - + case RECEIVED_ICON: if (mWebChromeClient != null) { mWebChromeClient.onReceivedIcon(mWebView, (Bitmap) msg.obj); @@ -340,7 +341,7 @@ class CallbackProxy extends Handler { case SSL_ERROR: if (mWebViewClient != null) { - HashMap<String, Object> map = + HashMap<String, Object> map = (HashMap<String, Object>) msg.obj; mWebViewClient.onReceivedSslError(mWebView, (SslErrorHandler) map.get("handler"), @@ -348,6 +349,16 @@ class CallbackProxy extends Handler { } break; + case CLIENT_CERT_REQUEST: + if (mWebViewClient != null) { + HashMap<String, Object> map = + (HashMap<String, Object>) msg.obj; + mWebViewClient.onReceivedClientCertRequest(mWebView, + (ClientCertRequestHandler) map.get("handler"), + (String) map.get("host_and_port")); + } + break; + case PROGRESS: // Synchronize to ensure mLatestProgress is not modified after // setProgress is called and before mProgressUpdatePending is @@ -543,14 +554,14 @@ class CallbackProxy extends Handler { new AlertDialog.Builder(mContext) .setTitle(getJsDialogTitle(url)) .setMessage(message) - .setPositiveButton(R.string.ok, + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick( DialogInterface dialog, int which) { res.confirm(); }}) - .setNegativeButton(R.string.cancel, + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick( DialogInterface dialog, @@ -904,7 +915,7 @@ class CallbackProxy extends Handler { if (PERF_PROBE) { // un-comment this if PERF_PROBE is true // Looper.myQueue().setWaitCallback(null); - Log.d("WebCore", "WebCore thread used " + + Log.d("WebCore", "WebCore thread used " + (SystemClock.currentThreadTimeMillis() - mWebCoreThreadTime) + " ms and idled " + mWebCoreIdleTime + " ms"); Network.getInstance(mContext).stopTiming(); @@ -934,7 +945,7 @@ class CallbackProxy extends Handler { sendMessage(msg); } - public void onFormResubmission(Message dontResend, + public void onFormResubmission(Message dontResend, Message resend) { // Do an unsynchronized quick check to avoid posting if no callback has // been set. @@ -998,7 +1009,6 @@ class CallbackProxy extends Handler { return; } Message msg = obtainMessage(SSL_ERROR); - //, handler); HashMap<String, Object> map = new HashMap(); map.put("handler", handler); map.put("error", error); @@ -1006,6 +1016,23 @@ class CallbackProxy extends Handler { sendMessage(msg); } /** + * @hide + */ + public void onReceivedClientCertRequest(ClientCertRequestHandler handler, String host_and_port) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + handler.cancel(); + return; + } + Message msg = obtainMessage(CLIENT_CERT_REQUEST); + HashMap<String, Object> map = new HashMap(); + map.put("handler", handler); + map.put("host_and_port", host_and_port); + msg.obj = map; + sendMessage(msg); + } + /** * @hide - hide this because it contains a parameter of type SslCertificate, * which is located in a hidden package. */ diff --git a/core/java/android/webkit/ClientCertRequestHandler.java b/core/java/android/webkit/ClientCertRequestHandler.java new file mode 100644 index 0000000..3a71e7e --- /dev/null +++ b/core/java/android/webkit/ClientCertRequestHandler.java @@ -0,0 +1,78 @@ +/* + * 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; + +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import org.apache.harmony.xnet.provider.jsse.NativeCrypto; + +/** + * ClientCertRequestHandler: class responsible for handling client + * certificate requests. This class is passed as a parameter to + * BrowserCallback.displayClientCertRequestDialog and is meant to + * receive the user's response. + * + * @hide + */ +public final class ClientCertRequestHandler { + + private final BrowserFrame mBrowserFrame; + private final int mHandle; + private final String mHostAndPort; + private final SslClientCertLookupTable mTable; + ClientCertRequestHandler(BrowserFrame browserFrame, + int handle, + String host_and_port, + SslClientCertLookupTable table) { + mBrowserFrame = browserFrame; + mHandle = handle; + mHostAndPort = host_and_port; + mTable = table; + } + + /** + * Proceed with the specified private key and client certificate chain. + */ + public void proceed(PrivateKey privateKey, X509Certificate[] chain) { + byte[] privateKeyBytes = privateKey.getEncoded(); + byte[][] chainBytes; + try { + chainBytes = NativeCrypto.encodeCertificates(chain); + } catch (CertificateEncodingException e) { + mBrowserFrame.nativeSslClientCert(mHandle, null, null); + return; + } + mTable.Allow(mHostAndPort, privateKeyBytes, chainBytes); + mBrowserFrame.nativeSslClientCert(mHandle, privateKeyBytes, chainBytes); + } + + /** + * Igore the request for now, the user may be prompted again. + */ + public void ignore() { + mBrowserFrame.nativeSslClientCert(mHandle, null, null); + } + + /** + * Cancel this request, remember the users negative choice. + */ + public void cancel() { + mTable.Deny(mHostAndPort); + mBrowserFrame.nativeSslClientCert(mHandle, null, null); + } +} diff --git a/core/java/android/webkit/DataLoader.java b/core/java/android/webkit/DataLoader.java index 235dc5be..e8d9069 100644 --- a/core/java/android/webkit/DataLoader.java +++ b/core/java/android/webkit/DataLoader.java @@ -22,7 +22,7 @@ import com.android.internal.R; import java.io.ByteArrayInputStream; -import org.apache.harmony.luni.util.Base64; +import libcore.io.Base64; /** * This class is a concrete implementation of StreamLoader that uses the diff --git a/core/java/android/webkit/HTML5VideoFullScreen.java b/core/java/android/webkit/HTML5VideoFullScreen.java index 0918683..57cda97 100644 --- a/core/java/android/webkit/HTML5VideoFullScreen.java +++ b/core/java/android/webkit/HTML5VideoFullScreen.java @@ -199,6 +199,9 @@ public class HTML5VideoFullScreen extends HTML5VideoView mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight); } + public boolean fullScreenExited() { + return (mLayout == null); + } private final WebChromeClient.CustomViewCallback mCallback = new WebChromeClient.CustomViewCallback() { @@ -208,7 +211,7 @@ public class HTML5VideoFullScreen extends HTML5VideoView // view. This happens in the WebChromeClient before this method // is invoked. pauseAndDispatch(mProxy); - + mProxy.dispatchOnStopFullScreen(); mLayout.removeView(getSurfaceView()); if (mProgressView != null) { @@ -253,7 +256,8 @@ public class HTML5VideoFullScreen extends HTML5VideoView client.onShowCustomView(mLayout, mCallback); // Plugins like Flash will draw over the video so hide // them while we're playing. - mProxy.getWebView().getViewManager().hideAll(); + if (webView.getViewManager() != null) + webView.getViewManager().hideAll(); mProgressView = client.getVideoLoadingProgressView(); if (mProgressView != null) { diff --git a/core/java/android/webkit/HTML5VideoInline.java b/core/java/android/webkit/HTML5VideoInline.java index 25921bc..ef1906c 100644 --- a/core/java/android/webkit/HTML5VideoInline.java +++ b/core/java/android/webkit/HTML5VideoInline.java @@ -12,10 +12,15 @@ import android.opengl.GLES20; */ public class HTML5VideoInline extends HTML5VideoView{ - // Due to the fact that SurfaceTexture consume a lot of memory, we make it - // as static. m_textureNames is the texture bound with this SurfaceTexture. + // Due to the fact that the decoder consume a lot of memory, we make the + // surface texture as singleton. But the GL texture (m_textureNames) + // associated with the surface texture can be used for showing the screen + // shot when paused, so they are not singleton. private static SurfaceTexture mSurfaceTexture = null; - private static int[] mTextureNames; + private int[] mTextureNames; + // Every time when the VideoLayer Id change, we need to recreate the + // SurfaceTexture in order to delete the old video's decoder memory. + private static int mVideoLayerUsingSurfaceTexture = -1; // Video control FUNCTIONS: @Override @@ -28,11 +33,12 @@ public class HTML5VideoInline extends HTML5VideoView{ HTML5VideoInline(int videoLayerId, int position, boolean autoStart) { init(videoLayerId, position, autoStart); + mTextureNames = null; } @Override public void decideDisplayMode() { - mPlayer.setTexture(getSurfaceTextureInstance()); + mPlayer.setTexture(getSurfaceTexture(getVideoLayerId())); } // Normally called immediately after setVideoURI. But for full screen, @@ -52,31 +58,38 @@ public class HTML5VideoInline extends HTML5VideoView{ // Inline Video specific FUNCTIONS: @Override - public SurfaceTexture getSurfaceTexture() { + public SurfaceTexture getSurfaceTexture(int videoLayerId) { + // Create the surface texture. + if (videoLayerId != mVideoLayerUsingSurfaceTexture + || mSurfaceTexture == null) { + if (mTextureNames == null) { + mTextureNames = new int[1]; + GLES20.glGenTextures(1, mTextureNames, 0); + } + mSurfaceTexture = new SurfaceTexture(mTextureNames[0]); + } + mVideoLayerUsingSurfaceTexture = videoLayerId; return mSurfaceTexture; } + public boolean surfaceTextureDeleted() { + return (mSurfaceTexture == null); + } + @Override public void deleteSurfaceTexture() { mSurfaceTexture = null; + mVideoLayerUsingSurfaceTexture = -1; return; } - // SurfaceTexture is a singleton here , too - private SurfaceTexture getSurfaceTextureInstance() { - // Create the surface texture. - if (mSurfaceTexture == null) - { - mTextureNames = new int[1]; - GLES20.glGenTextures(1, mTextureNames, 0); - mSurfaceTexture = new SurfaceTexture(mTextureNames[0]); - } - return mSurfaceTexture; - } - @Override public int getTextureName() { - return mTextureNames[0]; + if (mTextureNames != null) { + return mTextureNames[0]; + } else { + return 0; + } } private void setFrameAvailableListener(SurfaceTexture.OnFrameAvailableListener l) { diff --git a/core/java/android/webkit/HTML5VideoView.java b/core/java/android/webkit/HTML5VideoView.java index c05498a..5983a44 100644 --- a/core/java/android/webkit/HTML5VideoView.java +++ b/core/java/android/webkit/HTML5VideoView.java @@ -287,7 +287,7 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener { return false; } - public SurfaceTexture getSurfaceTexture() { + public SurfaceTexture getSurfaceTexture(int videoLayerId) { return null; } @@ -315,4 +315,14 @@ public class HTML5VideoView implements MediaPlayer.OnPreparedListener { // Only used in HTML5VideoFullScreen } + public boolean surfaceTextureDeleted() { + // Only meaningful for HTML5VideoInline + return false; + } + + public boolean fullScreenExited() { + // Only meaningful for HTML5VideoFullScreen + return false; + } + } diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java index d1b8cfc..d0237b5 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -65,6 +65,7 @@ class HTML5VideoViewProxy extends Handler private static final int ENDED = 201; private static final int POSTER_FETCHED = 202; private static final int PAUSED = 203; + private static final int STOPFULLSCREEN = 204; // Timer thread -> UI thread private static final int TIMEUPDATE = 300; @@ -105,12 +106,14 @@ class HTML5VideoViewProxy extends Handler public static void setBaseLayer(int layer) { // Don't do this for full screen mode. if (mHTML5VideoView != null - && !mHTML5VideoView.isFullScreenMode()) { + && !mHTML5VideoView.isFullScreenMode() + && !mHTML5VideoView.surfaceTextureDeleted()) { mBaseLayer = layer; - SurfaceTexture surfTexture = mHTML5VideoView.getSurfaceTexture(); - int textureName = mHTML5VideoView.getTextureName(); int currentVideoLayerId = mHTML5VideoView.getVideoLayerId(); + SurfaceTexture surfTexture = mHTML5VideoView.getSurfaceTexture(currentVideoLayerId); + int textureName = mHTML5VideoView.getTextureName(); + if (layer != 0 && surfTexture != null && currentVideoLayerId != -1) { int playerState = mHTML5VideoView.getCurrentState(); if (mHTML5VideoView.getPlayerBuffering()) @@ -170,14 +173,12 @@ class HTML5VideoViewProxy extends Handler boolean backFromFullScreenMode = false; if (mHTML5VideoView != null) { currentVideoLayerId = mHTML5VideoView.getVideoLayerId(); - if (mHTML5VideoView instanceof HTML5VideoFullScreen) { - backFromFullScreenMode = true; - } + backFromFullScreenMode = mHTML5VideoView.fullScreenExited(); } if (backFromFullScreenMode - || currentVideoLayerId != videoLayerId - || mHTML5VideoView.getSurfaceTexture() == null) { + || currentVideoLayerId != videoLayerId + || mHTML5VideoView.surfaceTextureDeleted()) { // Here, we handle the case when switching to a new video, // either inside a WebView or across WebViews // For switching videos within a WebView or across the WebView, @@ -287,8 +288,13 @@ class HTML5VideoViewProxy extends Handler } public void dispatchOnPaused() { - Message msg = Message.obtain(mWebCoreHandler, PAUSED); - mWebCoreHandler.sendMessage(msg); + Message msg = Message.obtain(mWebCoreHandler, PAUSED); + mWebCoreHandler.sendMessage(msg); + } + + public void dispatchOnStopFullScreen() { + Message msg = Message.obtain(mWebCoreHandler, STOPFULLSCREEN); + mWebCoreHandler.sendMessage(msg); } public void onTimeupdate() { @@ -560,6 +566,9 @@ class HTML5VideoViewProxy extends Handler case TIMEUPDATE: nativeOnTimeupdate(msg.arg1, mNativePointer); break; + case STOPFULLSCREEN: + nativeOnStopFullscreen(mNativePointer); + break; } } }; @@ -686,6 +695,7 @@ class HTML5VideoViewProxy extends Handler private native void nativeOnPaused(int nativePointer); private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); private native void nativeOnTimeupdate(int position, int nativePointer); + private native void nativeOnStopFullscreen(int nativePointer); private native static boolean nativeSendSurfaceTexture(SurfaceTexture texture, int baseLayer, int videoLayerId, int textureName, int playerState); diff --git a/core/java/android/webkit/JniUtil.java b/core/java/android/webkit/JniUtil.java index 62b415c..b5d4933 100644 --- a/core/java/android/webkit/JniUtil.java +++ b/core/java/android/webkit/JniUtil.java @@ -48,6 +48,12 @@ class JniUtil { initialized = true; } + protected static synchronized Context getContext() { + if (!initialized) + return null; + return sContext; + } + /** * Called by JNI. Gets the application's database directory, excluding the trailing slash. * @return String The application's database directory diff --git a/core/java/android/webkit/L10nUtils.java b/core/java/android/webkit/L10nUtils.java index f59d7d0..5b4fb1d 100644 --- a/core/java/android/webkit/L10nUtils.java +++ b/core/java/android/webkit/L10nUtils.java @@ -69,7 +69,8 @@ public class L10nUtils { com.android.internal.R.string.autofill_card_number_re, // IDS_AUTOFILL_CARD_NUMBER_RE com.android.internal.R.string.autofill_expiration_month_re, // IDS_AUTOFILL_EXPIRATION_MONTH_RE com.android.internal.R.string.autofill_expiration_date_re, // IDS_AUTOFILL_EXPIRATION_DATE_RE - com.android.internal.R.string.autofill_card_ignored_re // IDS_AUTOFILL_CARD_IGNORED_RE + com.android.internal.R.string.autofill_card_ignored_re, // IDS_AUTOFILL_CARD_IGNORED_RE + com.android.internal.R.string.autofill_fax_re // IDS_AUTOFILL_FAX_RE }; private static Context mApplicationContext; diff --git a/core/java/android/webkit/PluginFullScreenHolder.java b/core/java/android/webkit/PluginFullScreenHolder.java index ae326d5..42ba7c9 100644 --- a/core/java/android/webkit/PluginFullScreenHolder.java +++ b/core/java/android/webkit/PluginFullScreenHolder.java @@ -24,34 +24,44 @@ */ package android.webkit; -import android.app.Dialog; +import android.content.Context; +import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; -class PluginFullScreenHolder extends Dialog { +class PluginFullScreenHolder { private final WebView mWebView; private final int mNpp; + private final int mOrientation; + + // The container for the plugin view + private static CustomFrameLayout mLayout; + private View mContentView; - PluginFullScreenHolder(WebView webView, int npp) { - super(webView.getContext(), android.R.style.Theme_NoTitleBar_Fullscreen); + PluginFullScreenHolder(WebView webView, int orientation, int npp) { mWebView = webView; mNpp = npp; + mOrientation = orientation; } - @Override public void setContentView(View contentView) { - // as we are sharing the View between full screen and - // embedded mode, we have to remove the - // AbsoluteLayout.LayoutParams set by embedded mode to - // ViewGroup.LayoutParams before adding it to the dialog - contentView.setLayoutParams(new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); + + // Create a FrameLayout that will contain the plugin's view + mLayout = new CustomFrameLayout(mWebView.getContext()); + FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + Gravity.CENTER); + + mLayout.addView(contentView, layoutParams); + mLayout.setVisibility(View.VISIBLE); + // fixed size is only used either during pinch zoom or surface is too // big. Make sure it is not fixed size before setting it to the full // screen content view. The SurfaceView will be set to the correct mode @@ -62,59 +72,79 @@ class PluginFullScreenHolder extends Dialog { sView.getHolder().setSizeFromLayout(); } } - super.setContentView(contentView); + mContentView = contentView; } - @Override - public void onBackPressed() { - mWebView.mPrivateHandler.obtainMessage(WebView.HIDE_FULLSCREEN) - .sendToTarget(); + public void show() { + // Other plugins may attempt to draw so hide them while we're active. + if (mWebView.getViewManager() != null) + mWebView.getViewManager().hideAll(); + + WebChromeClient client = mWebView.getWebChromeClient(); + client.onShowCustomView(mLayout, mOrientation, mCallback); } - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (event.isSystem()) { - return super.onKeyDown(keyCode, event); - } - mWebView.onKeyDown(keyCode, event); - // always return true as we are the handler - return true; + public void hide() { + WebChromeClient client = mWebView.getWebChromeClient(); + client.onHideCustomView(); } - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (event.isSystem()) { - return super.onKeyUp(keyCode, event); + private class CustomFrameLayout extends FrameLayout { + + CustomFrameLayout(Context context) { + super(context); } - mWebView.onKeyUp(keyCode, event); - // always return true as we are the handler - return true; - } - @Override - public boolean onTouchEvent(MotionEvent event) { - // always return true as we don't want the event to propagate any further - return true; - } + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (event.isSystem()) { + return super.onKeyDown(keyCode, event); + } + mWebView.onKeyDown(keyCode, event); + // always return true as we are the handler + return true; + } - @Override - public boolean onTrackballEvent(MotionEvent event) { - mWebView.onTrackballEvent(event); - // always return true as we are the handler - return true; - } + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (event.isSystem()) { + return super.onKeyUp(keyCode, event); + } + mWebView.onKeyUp(keyCode, event); + // always return true as we are the handler + return true; + } - @Override - protected void onStop() { - super.onStop(); - // manually remove the contentView's parent since the dialog does not - if (mContentView != null && mContentView.getParent() != null) { - ViewGroup vg = (ViewGroup) mContentView.getParent(); - vg.removeView(mContentView); + @Override + public boolean onTouchEvent(MotionEvent event) { + // always return true as we don't want the event to propagate any further + return true; + } + + @Override + public boolean onTrackballEvent(MotionEvent event) { + mWebView.onTrackballEvent(event); + // always return true as we are the handler + return true; } - mWebView.getWebViewCore().sendMessage( - WebViewCore.EventHub.HIDE_FULLSCREEN, mNpp, 0); } + + private final WebChromeClient.CustomViewCallback mCallback = + new WebChromeClient.CustomViewCallback() { + public void onCustomViewHidden() { + + mWebView.mPrivateHandler.obtainMessage(WebView.HIDE_FULLSCREEN) + .sendToTarget(); + + mWebView.getWebViewCore().sendMessage( + WebViewCore.EventHub.HIDE_FULLSCREEN, mNpp, 0); + mLayout.removeView(mContentView); + mLayout = null; + + // Re enable plugin views. + mWebView.getViewManager().showAll(); + } + }; } diff --git a/core/java/android/webkit/SslCertLookupTable.java b/core/java/android/webkit/SslCertLookupTable.java index abf612e..faff110 100644 --- a/core/java/android/webkit/SslCertLookupTable.java +++ b/core/java/android/webkit/SslCertLookupTable.java @@ -19,11 +19,11 @@ package android.webkit; import android.os.Bundle; import android.net.http.SslError; -/* +/** * A simple class to store the wrong certificates that user is aware but * chose to proceed. */ -class SslCertLookupTable { +final class SslCertLookupTable { private static SslCertLookupTable sTable; private final Bundle table; diff --git a/core/java/android/webkit/SslClientCertLookupTable.java b/core/java/android/webkit/SslClientCertLookupTable.java new file mode 100644 index 0000000..630debd --- /dev/null +++ b/core/java/android/webkit/SslClientCertLookupTable.java @@ -0,0 +1,73 @@ +/* + * 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; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A simple class to store client certificates that user has chosen. + */ +final class SslClientCertLookupTable { + private static SslClientCertLookupTable sTable; + private final Map<String, byte[]> privateKeys; + private final Map<String, byte[][]> certificateChains; + private final Set<String> denied; + + public static synchronized SslClientCertLookupTable getInstance() { + if (sTable == null) { + sTable = new SslClientCertLookupTable(); + } + return sTable; + } + + private SslClientCertLookupTable() { + privateKeys = new HashMap<String, byte[]>(); + certificateChains = new HashMap<String, byte[][]>(); + denied = new HashSet<String>(); + } + + public void Allow(String host_and_port, byte[] privateKey, byte[][] chain) { + privateKeys.put(host_and_port, privateKey); + certificateChains.put(host_and_port, chain); + denied.remove(host_and_port); + } + + public void Deny(String host_and_port) { + privateKeys.remove(host_and_port); + certificateChains.remove(host_and_port); + denied.add(host_and_port); + } + + public boolean IsAllowed(String host_and_port) { + return privateKeys.containsKey(host_and_port); + } + + public boolean IsDenied(String host_and_port) { + return denied.contains(host_and_port); + } + + public byte[] PrivateKey(String host_and_port) { + return privateKeys.get(host_and_port); + } + + public byte[][] CertificateChain(String host_and_port) { + return certificateChains.get(host_and_port); + } +} diff --git a/core/java/android/webkit/ViewStateSerializer.java b/core/java/android/webkit/ViewStateSerializer.java new file mode 100644 index 0000000..0fc76fa --- /dev/null +++ b/core/java/android/webkit/ViewStateSerializer.java @@ -0,0 +1,79 @@ +/* + * 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; + +import android.graphics.Point; +import android.graphics.Region; +import android.webkit.WebViewCore.DrawData; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @hide + */ +class ViewStateSerializer { + + private static final int WORKING_STREAM_STORAGE = 16 * 1024; + + static final int VERSION = 1; + + static boolean serializeViewState(OutputStream stream, WebView web) + throws IOException { + DataOutputStream dos = new DataOutputStream(stream); + dos.writeInt(VERSION); + dos.writeInt(web.getContentWidth()); + dos.writeInt(web.getContentHeight()); + return nativeSerializeViewState(web.getBaseLayer(), dos, + new byte[WORKING_STREAM_STORAGE]); + } + + static DrawData deserializeViewState(InputStream stream, WebView web) + throws IOException { + DataInputStream dis = new DataInputStream(stream); + int version = dis.readInt(); + if (version != VERSION) { + throw new IOException("Unexpected version: " + version); + } + int contentWidth = dis.readInt(); + int contentHeight = dis.readInt(); + int baseLayer = nativeDeserializeViewState(dis, + new byte[WORKING_STREAM_STORAGE]); + + final WebViewCore.DrawData draw = new WebViewCore.DrawData(); + draw.mViewState = new WebViewCore.ViewState(); + int viewWidth = web.getViewWidth(); + int viewHeight = web.getViewHeightWithTitle() - web.getTitleHeight(); + draw.mViewSize = new Point(viewWidth, viewHeight); + draw.mContentSize = new Point(contentWidth, contentHeight); + draw.mViewState.mDefaultScale = web.getDefaultZoomScale(); + draw.mBaseLayer = baseLayer; + draw.mInvalRegion = new Region(0, 0, contentWidth, contentHeight); + return draw; + } + + private static native boolean nativeSerializeViewState(int baseLayer, + OutputStream stream, byte[] storage); + + // Returns a pointer to the BaseLayer + private static native int nativeDeserializeViewState( + InputStream stream, byte[] storage); + + private ViewStateSerializer() {} +} diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 755366c..ae40ded 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -16,6 +16,7 @@ package android.webkit; +import android.content.pm.ActivityInfo; import android.graphics.Bitmap; import android.net.Uri; import android.os.Message; @@ -77,6 +78,18 @@ public class WebChromeClient { /** * Notify the host application that the current page would + * like to show a custom View in a particular orientation. + * @param view is the View object to be shown. + * @param requestedOrientation An orientation constant as used in + * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}. + * @param callback is the callback to be invoked if and when the view + * is dismissed. + */ + public void onShowCustomView(View view, int requestedOrientation, + CustomViewCallback callback) {}; + + /** + * Notify the host application that the current page would * like to hide its custom view. */ public void onHideCustomView() {} diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 8e57260..c361a4a 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -183,7 +183,6 @@ public class WebSettings { private boolean mJavaScriptCanOpenWindowsAutomatically = false; private boolean mUseDoubleTree = false; private boolean mUseWideViewport = false; - private boolean mUseFixedViewport = false; private boolean mSupportMultipleWindows = false; private boolean mShrinksStandaloneImagesToFit = false; private long mMaximumDecodedImageSize = 0; // 0 means default @@ -223,6 +222,7 @@ public class WebSettings { private boolean mAllowContentAccess = true; private boolean mLoadWithOverviewMode = false; private boolean mEnableSmoothTransition = false; + private boolean mForceUserScalable = false; // AutoFill Profile data /** @@ -381,13 +381,6 @@ public class WebSettings { mDefaultTextEncoding = context.getString(com.android.internal. R.string.default_text_encoding); - // Detect tablet device for fixed viewport mode. - final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - final int landscapeWidth = Math.max(metrics.widthPixels, metrics.heightPixels); - final int minTabletWidth = context.getResources().getDimensionPixelSize( - com.android.internal.R.dimen.min_xlarge_screen_width); - mUseFixedViewport = (metrics.density == 1.0f && landscapeWidth >= minTabletWidth); - if (sLockForLocaleSettings == null) { sLockForLocaleSettings = new Object(); sLocale = Locale.getDefault(); @@ -1650,11 +1643,11 @@ public class WebSettings { } /** - * Returns whether to use fixed viewport. Fixed viewport should operate only - * when wide viewport is on. + * Returns whether to use fixed viewport. Use fixed viewport + * whenever wide viewport is on. */ /* package */ boolean getUseFixedViewport() { - return getUseWideViewPort() && mUseFixedViewport; + return getUseWideViewPort(); } /** @@ -1680,6 +1673,23 @@ public class WebSettings { } } + /** + * Returns whether the viewport metatag can disable zooming + * @hide + */ + public boolean forceUserScalable() { + return mForceUserScalable; + } + + /** + * Sets whether viewport metatag can disable zooming. + * @param flag Whether or not to forceably enable user scalable. + * @hide + */ + public synchronized void setForceUserScalable(boolean flag) { + mForceUserScalable = flag; + } + synchronized void setSyntheticLinksEnabled(boolean flag) { if (mSyntheticLinksEnabled != flag) { mSyntheticLinksEnabled = flag; diff --git a/core/java/android/webkit/WebTextView.java b/core/java/android/webkit/WebTextView.java index 26cfbff..d54230c 100644 --- a/core/java/android/webkit/WebTextView.java +++ b/core/java/android/webkit/WebTextView.java @@ -34,6 +34,7 @@ import android.text.BoringLayout.Metrics; import android.text.DynamicLayout; import android.text.Editable; import android.text.InputFilter; +import android.text.InputType; import android.text.Layout; import android.text.Selection; import android.text.Spannable; @@ -51,8 +52,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 +516,6 @@ import junit.framework.Assert; int candEnd = EditableInputConnection.getComposingSpanEnd(sp); imm.updateSelection(this, selStart, selEnd, candStart, candEnd); } - updateCursorControllerPositions(); } @Override @@ -854,7 +854,7 @@ import junit.framework.Assert; public void setAdapterCustom(AutoCompleteAdapter adapter) { if (adapter != null) { setInputType(getInputType() - | EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE); + | InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE); adapter.setTextView(this); if (mAutoFillable) { setOnItemClickListener(this); @@ -935,7 +935,7 @@ import junit.framework.Assert; */ /* package */ void setInPassword(boolean inPassword) { if (inPassword) { - setInputType(EditorInfo.TYPE_CLASS_TEXT | EditorInfo. + setInputType(InputType.TYPE_CLASS_TEXT | EditorInfo. TYPE_TEXT_VARIATION_WEB_PASSWORD); createBackground(); } @@ -1147,8 +1147,8 @@ import junit.framework.Assert; boolean single = true; boolean inPassword = false; int maxLength = -1; - int inputType = EditorInfo.TYPE_CLASS_TEXT - | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; + int inputType = InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; int imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN; if (TEXT_AREA != type @@ -1161,9 +1161,9 @@ import junit.framework.Assert; break; case TEXT_AREA: single = false; - inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE - | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES - | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; + inputType |= InputType.TYPE_TEXT_FLAG_MULTI_LINE + | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; imeOptions |= EditorInfo.IME_ACTION_NONE; break; case PASSWORD: @@ -1175,20 +1175,20 @@ import junit.framework.Assert; break; case EMAIL: // inputType needs to be overwritten because of the different text variation. - inputType = EditorInfo.TYPE_CLASS_TEXT - | EditorInfo.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; + inputType = InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS; imeOptions |= EditorInfo.IME_ACTION_GO; break; case NUMBER: // inputType needs to be overwritten because of the different class. - inputType = EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_NORMAL; + inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_NORMAL; // Number and telephone do not have both a Tab key and an // action, so set the action to NEXT imeOptions |= EditorInfo.IME_ACTION_NEXT; break; case TELEPHONE: // inputType needs to be overwritten because of the different class. - inputType = EditorInfo.TYPE_CLASS_PHONE; + inputType = InputType.TYPE_CLASS_PHONE; imeOptions |= EditorInfo.IME_ACTION_NEXT; break; case URL: diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index eeb5b7b..1014d7e 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; @@ -43,7 +44,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; -import android.graphics.SurfaceTexture; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.net.Proxy; @@ -53,7 +53,9 @@ import android.net.http.SslCertificate; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.Message; +import android.os.StrictMode; import android.provider.Settings; import android.speech.tts.TextToSpeech; import android.text.Selection; @@ -81,6 +83,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.webkit.WebTextView.AutoCompleteAdapter; +import android.webkit.WebViewCore.DrawData; import android.webkit.WebViewCore.EventHub; import android.webkit.WebViewCore.TouchEventData; import android.webkit.WebViewCore.TouchHighlightData; @@ -95,10 +98,15 @@ import android.widget.ListView; import android.widget.OverScroller; import android.widget.Toast; +import junit.framework.Assert; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; @@ -110,8 +118,6 @@ import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; -import junit.framework.Assert; - /** * <p>A View that displays web pages. This class is the basis upon which you * can roll your own web browser or simply display some online content within your Activity. @@ -166,7 +172,7 @@ import junit.framework.Assert; * * // OR, you can also load from an HTML string: * String summary = "<html><body>You scored <b>192</b> points.</body></html>"; - * webview.loadData(summary, "text/html", "utf-8"); + * webview.loadData(summary, "text/html", null); * // ... although note that there are restrictions on what this HTML can do. * // See the JavaDocs for {@link #loadData(String,String,String) loadData()} and {@link * #loadDataWithBaseURL(String,String,String,String,String) loadDataWithBaseURL()} for more info. @@ -613,6 +619,9 @@ public class WebView extends AbsoluteLayout // SetBaseLayer time and to pause when WebView paused. private HTML5VideoViewProxy mHTML5VideoViewProxy; + // If we are using a set picture, don't send view updates to webkit + private boolean mBlockWebkitViewMessages = false; + /* * Private message ids */ @@ -821,6 +830,8 @@ public class WebView extends AbsoluteLayout private WebViewCore.AutoFillData mAutoFillData; + private static boolean sNotificationsEnabled = true; + /** * URI scheme for telephone number */ @@ -874,8 +885,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 +906,9 @@ public class WebView extends AbsoluteLayout */ public static final int IMAGE_TYPE = 5; /** - * HitTestResult for hitting a HTML::a tag which contains HTML::img + * @deprecated This type is no longer used. */ + @Deprecated public static final int IMAGE_ANCHOR_TYPE = 6; /** * HitTestResult for hitting a HTML::a tag with src=http @@ -987,6 +1000,7 @@ public class WebView extends AbsoluteLayout protected WebView(Context context, AttributeSet attrs, int defStyle, Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) { super(context, attrs, defStyle); + checkThread(); // Used by the chrome stack to find application paths JniUtil.setContext(context); @@ -1024,24 +1038,38 @@ public class WebView extends AbsoluteLayout } /* - * A variable to track if there is a receiver added for PROXY_CHANGE_ACTION + * Receiver for PROXY_CHANGE_ACTION, will be null when it is not added handling broadcasts. */ - private static boolean sProxyReceiverAdded; + private static ProxyReceiver sProxyReceiver; + /* + * @param context This method expects this to be a valid context + */ private static synchronized void setupProxyListener(Context context) { - if (sProxyReceiverAdded) { + if (sProxyReceiver != null || sNotificationsEnabled == false) { return; } IntentFilter filter = new IntentFilter(); filter.addAction(Proxy.PROXY_CHANGE_ACTION); + sProxyReceiver = new ProxyReceiver(); Intent currentProxy = context.getApplicationContext().registerReceiver( - new ProxyReceiver(), filter); - sProxyReceiverAdded = true; + sProxyReceiver, filter); if (currentProxy != null) { handleProxyBroadcast(currentProxy); } } + /* + * @param context This method expects this to be a valid context + */ + private static synchronized void disableProxyListener(Context context) { + if (sProxyReceiver == null) + return; + + context.getApplicationContext().unregisterReceiver(sProxyReceiver); + sProxyReceiver = null; + } + private static void handleProxyBroadcast(Intent intent) { ProxyProperties proxyProperties = (ProxyProperties)intent.getExtra(Proxy.EXTRA_PROXY_INFO); if (proxyProperties == null || proxyProperties.getHost() == null) { @@ -1129,7 +1157,7 @@ public class WebView extends AbsoluteLayout PackageInfo pInfo = pm.getPackageInfo(name, PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES); installedPackages.add(name); - } catch(PackageManager.NameNotFoundException e) { + } catch (PackageManager.NameNotFoundException e) { // package not found } } @@ -1184,6 +1212,11 @@ public class WebView extends AbsoluteLayout mHTML5VideoViewProxy = null ; } + @Override + public boolean shouldDelayChildPressedState() { + return true; + } + /** * Adds accessibility APIs to JavaScript. * @@ -1300,6 +1333,7 @@ public class WebView extends AbsoluteLayout * @param overlay TRUE if horizontal scrollbar should have overlay style. */ public void setHorizontalScrollbarOverlay(boolean overlay) { + checkThread(); mOverlayHorizontalScrollbar = overlay; } @@ -1308,6 +1342,7 @@ public class WebView extends AbsoluteLayout * @param overlay TRUE if vertical scrollbar should have overlay style. */ public void setVerticalScrollbarOverlay(boolean overlay) { + checkThread(); mOverlayVerticalScrollbar = overlay; } @@ -1316,6 +1351,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if horizontal scrollbar has overlay style. */ public boolean overlayHorizontalScrollbar() { + checkThread(); return mOverlayHorizontalScrollbar; } @@ -1324,6 +1360,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if vertical scrollbar has overlay style. */ public boolean overlayVerticalScrollbar() { + checkThread(); return mOverlayVerticalScrollbar; } @@ -1355,6 +1392,7 @@ public class WebView extends AbsoluteLayout * @deprecated This method is now obsolete. */ public int getVisibleTitleHeight() { + checkThread(); // need to restrict mScrollY due to over scroll return Math.max(getTitleHeight() - Math.max(0, mScrollY), 0); } @@ -1368,7 +1406,7 @@ public class WebView extends AbsoluteLayout return getViewHeightWithTitle() - getVisibleTitleHeight(); } - private int getViewHeightWithTitle() { + int getViewHeightWithTitle() { int height = getHeight(); if (isHorizontalScrollBarEnabled() && !mOverlayHorizontalScrollbar) { height -= getHorizontalScrollbarHeight(); @@ -1381,6 +1419,7 @@ public class WebView extends AbsoluteLayout * there is no certificate (the site is not secure). */ public SslCertificate getCertificate() { + checkThread(); return mCertificate; } @@ -1388,6 +1427,7 @@ public class WebView extends AbsoluteLayout * Sets the SSL certificate for the main top-level page. */ public void setCertificate(SslCertificate certificate) { + checkThread(); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "setCertificate=" + certificate); } @@ -1407,6 +1447,7 @@ public class WebView extends AbsoluteLayout * @param password The password for the given host. */ public void savePassword(String host, String username, String password) { + checkThread(); mDatabase.setUsernamePassword(host, username, password); } @@ -1421,6 +1462,7 @@ public class WebView extends AbsoluteLayout */ public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { + checkThread(); mDatabase.setHttpAuthUsernamePassword(host, realm, username, password); } @@ -1434,6 +1476,7 @@ public class WebView extends AbsoluteLayout * String[1] is password. Return null if it can't find anything. */ public String[] getHttpAuthUsernamePassword(String host, String realm) { + checkThread(); return mDatabase.getHttpAuthUsernamePassword(host, realm); } @@ -1466,6 +1509,11 @@ public class WebView extends AbsoluteLayout * methods may be called on a WebView after destroy. */ public void destroy() { + checkThread(); + destroyImpl(); + } + + private void destroyImpl() { clearHelpers(); if (mListBoxDialog != null) { mListBoxDialog.dismiss(); @@ -1500,21 +1548,38 @@ public class WebView extends AbsoluteLayout /** * Enables platform notifications of data state and proxy changes. - * @deprecated Obsolete - platform notifications are always enabled. + * Notifications are enabled by default. + * + * @deprecated This method is now obsolete. */ @Deprecated public static void enablePlatformNotifications() { - Network.enablePlatformNotifications(); + checkThread(); + synchronized (WebView.class) { + Network.enablePlatformNotifications(); + sNotificationsEnabled = true; + Context context = JniUtil.getContext(); + if (context != null) + setupProxyListener(context); + } } /** - * If platform notifications are enabled, this should be called - * from the Activity's onPause() or onStop(). - * @deprecated Obsolete - platform notifications are always enabled. + * Disables platform notifications of data state and proxy changes. + * Notifications are enabled by default. + * + * @deprecated This method is now obsolete. */ @Deprecated public static void disablePlatformNotifications() { - Network.disablePlatformNotifications(); + checkThread(); + synchronized (WebView.class) { + Network.disablePlatformNotifications(); + sNotificationsEnabled = false; + Context context = JniUtil.getContext(); + if (context != null) + disableProxyListener(context); + } } /** @@ -1525,6 +1590,7 @@ public class WebView extends AbsoluteLayout * @hide pending API solidification */ public void setJsFlags(String flags) { + checkThread(); mWebViewCore.sendMessage(EventHub.SET_JS_FLAGS, flags); } @@ -1535,6 +1601,7 @@ public class WebView extends AbsoluteLayout * @param networkUp boolean indicating if network is available */ public void setNetworkAvailable(boolean networkUp) { + checkThread(); mWebViewCore.sendMessage(EventHub.SET_NETWORK_STATE, networkUp ? 1 : 0, 0); } @@ -1544,6 +1611,7 @@ public class WebView extends AbsoluteLayout * {@hide} */ public void setNetworkType(String type, String subtype) { + checkThread(); Map<String, String> map = new HashMap<String, String>(); map.put("type", type); map.put("subtype", subtype); @@ -1563,6 +1631,7 @@ public class WebView extends AbsoluteLayout * @see #restorePicture */ public WebBackForwardList saveState(Bundle outState) { + checkThread(); if (outState == null) { return null; } @@ -1619,6 +1688,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public boolean savePicture(Bundle b, final File dest) { + checkThread(); if (dest == null || b == null) { return false; } @@ -1683,6 +1753,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public boolean restorePicture(Bundle b, File src) { + checkThread(); if (src == null || b == null) { return false; } @@ -1721,6 +1792,53 @@ public class WebView extends AbsoluteLayout } /** + * Saves the view data to the output stream. The output is highly + * version specific, and may not be able to be loaded by newer versions + * of WebView. + * @param stream The {@link OutputStream} to save to + * @return True if saved successfully + * @hide + */ + public boolean saveViewState(OutputStream stream) { + try { + return ViewStateSerializer.serializeViewState(stream, this); + } catch (IOException e) { + Log.w(LOGTAG, "Failed to saveViewState", e); + } + return false; + } + + /** + * Loads the view data from the input stream. See + * {@link #saveViewState(OutputStream)} for more information. + * @param stream The {@link InputStream} to load from + * @return True if loaded successfully + * @hide + */ + public boolean loadViewState(InputStream stream) { + try { + mLoadedPicture = ViewStateSerializer.deserializeViewState(stream, this); + mBlockWebkitViewMessages = true; + setNewPicture(mLoadedPicture, true); + return true; + } catch (IOException e) { + Log.w(LOGTAG, "Failed to loadViewState", e); + } + return false; + } + + /** + * Clears the view state set with {@link #loadViewState(InputStream)}. + * This WebView will then switch to showing the content from webkit + * @hide + */ + public void clearViewState() { + mBlockWebkitViewMessages = false; + mLoadedPicture = null; + invalidate(); + } + + /** * Restore the state of this WebView from the given map used in * {@link android.app.Activity#onRestoreInstanceState}. This method should * be called to restore the state of the WebView before using the object. If @@ -1735,6 +1853,7 @@ public class WebView extends AbsoluteLayout * @see #restorePicture */ public WebBackForwardList restoreState(Bundle inState) { + checkThread(); WebBackForwardList returnList = null; if (inState == null) { return returnList; @@ -1794,6 +1913,11 @@ public class WebView extends AbsoluteLayout * will be replaced by the intrinsic value of the WebView. */ public void loadUrl(String url, Map<String, String> extraHeaders) { + checkThread(); + loadUrlImpl(url, extraHeaders); + } + + private void loadUrlImpl(String url, Map<String, String> extraHeaders) { switchOutDrawHistory(); WebViewCore.GetUrlData arg = new WebViewCore.GetUrlData(); arg.mUrl = url; @@ -1807,10 +1931,15 @@ public class WebView extends AbsoluteLayout * @param url The url of the resource to load. */ public void loadUrl(String url) { + checkThread(); + loadUrlImpl(url); + } + + private void loadUrlImpl(String url) { if (url == null) { return; } - loadUrl(url, null); + loadUrlImpl(url, null); } /** @@ -1822,6 +1951,7 @@ public class WebView extends AbsoluteLayout * @param postData The data will be passed to "POST" request. */ public void postUrl(String url, byte[] postData) { + checkThread(); if (URLUtil.isNetworkUrl(url)) { switchOutDrawHistory(); WebViewCore.PostUrlData arg = new WebViewCore.PostUrlData(); @@ -1830,22 +1960,37 @@ public class WebView extends AbsoluteLayout mWebViewCore.sendMessage(EventHub.POST_URL, arg); clearHelpers(); } else { - loadUrl(url); + loadUrlImpl(url); } } /** - * Load the given data into the WebView. This will load the data into - * WebView using the data: scheme. Content loaded through this mechanism - * does not have the ability to load content from the network. - * @param data A String of data in the given encoding. The date must - * be URI-escaped -- '#', '%', '\', '?' should be replaced by %23, %25, - * %27, %3f respectively. - * @param mimeType The MIMEType of the data. i.e. text/html, image/jpeg - * @param encoding The encoding of the data. i.e. utf-8, base64 + * Load the given data into the WebView using a 'data' scheme URL. Content + * loaded in this way does not have the ability to load content from the + * network. + * <p> + * If the value of the encoding parameter is 'base64', then the data must + * be encoded as base64. Otherwise, the data must use ASCII encoding for + * octets inside the range of safe URL characters and use the standard %xx + * hex encoding of URLs for octets outside that range. + * @param data A String of data in the given encoding. + * @param mimeType The MIMEType of the data, e.g. 'text/html'. + * @param encoding The encoding of the data. */ public void loadData(String data, String mimeType, String encoding) { - loadUrl("data:" + mimeType + ";" + encoding + "," + data); + checkThread(); + loadDataImpl(data, mimeType, encoding); + } + + private void loadDataImpl(String data, String mimeType, String encoding) { + StringBuilder dataUrl = new StringBuilder("data:"); + dataUrl.append(mimeType); + if ("base64".equals(encoding)) { + dataUrl.append(";base64"); + } + dataUrl.append(","); + dataUrl.append(data); + loadUrlImpl(dataUrl.toString()); } /** @@ -1854,13 +1999,9 @@ public class WebView extends AbsoluteLayout * that is loaded through this interface. As such, it is used to resolve any * relative URLs. The historyUrl is used for the history entry. * <p> - * Note for post 1.0. Due to the change in the WebKit, the access to asset - * files through "file:///android_asset/" for the sub resources is more - * restricted. If you provide null or empty string as baseUrl, you won't be - * able to access asset files. If the baseUrl is anything other than - * http(s)/ftp(s)/about/javascript as scheme, you can access asset files for - * sub resources. - * + * Note that content specified in this way can access local device files + * (via 'file' scheme URLs) only if baseUrl specifies a scheme other than + * 'http', 'https', 'ftp', 'ftps', 'about' or 'javascript'. * @param baseUrl Url to resolve relative paths with, if null defaults to * "about:blank" * @param data A String of data in the given encoding. @@ -1871,9 +2012,10 @@ public class WebView extends AbsoluteLayout */ public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { + checkThread(); if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) { - loadData(data, mimeType, encoding); + loadDataImpl(data, mimeType, encoding); return; } switchOutDrawHistory(); @@ -1893,7 +2035,8 @@ public class WebView extends AbsoluteLayout * @param filename The filename where the archive should be placed. */ public void saveWebArchive(String filename) { - saveWebArchive(filename, false, null); + checkThread(); + saveWebArchiveImpl(filename, false, null); } /* package */ static class SaveWebArchiveMessage { @@ -1922,6 +2065,12 @@ public class WebView extends AbsoluteLayout * file failed. */ public void saveWebArchive(String basename, boolean autoname, ValueCallback<String> callback) { + checkThread(); + saveWebArchiveImpl(basename, autoname, callback); + } + + private void saveWebArchiveImpl(String basename, boolean autoname, + ValueCallback<String> callback) { mWebViewCore.sendMessage(EventHub.SAVE_WEBARCHIVE, new SaveWebArchiveMessage(basename, autoname, callback)); } @@ -1930,6 +2079,7 @@ public class WebView extends AbsoluteLayout * Stop the current load. */ public void stopLoading() { + checkThread(); // TODO: should we clear all the messages in the queue before sending // STOP_LOADING? switchOutDrawHistory(); @@ -1940,6 +2090,7 @@ public class WebView extends AbsoluteLayout * Reload the current url. */ public void reload() { + checkThread(); clearHelpers(); switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.RELOAD); @@ -1950,6 +2101,7 @@ public class WebView extends AbsoluteLayout * @return True iff this WebView has a back history item. */ public boolean canGoBack() { + checkThread(); WebBackForwardList l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { @@ -1964,7 +2116,8 @@ public class WebView extends AbsoluteLayout * Go back in the history of this WebView. */ public void goBack() { - goBackOrForward(-1); + checkThread(); + goBackOrForwardImpl(-1); } /** @@ -1972,6 +2125,7 @@ public class WebView extends AbsoluteLayout * @return True iff this Webview has a forward history item. */ public boolean canGoForward() { + checkThread(); WebBackForwardList l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { @@ -1986,7 +2140,8 @@ public class WebView extends AbsoluteLayout * Go forward in the history of this WebView. */ public void goForward() { - goBackOrForward(1); + checkThread(); + goBackOrForwardImpl(1); } /** @@ -1996,6 +2151,7 @@ public class WebView extends AbsoluteLayout * history. */ public boolean canGoBackOrForward(int steps) { + checkThread(); WebBackForwardList l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { @@ -2015,6 +2171,11 @@ public class WebView extends AbsoluteLayout * forward list. */ public void goBackOrForward(int steps) { + checkThread(); + goBackOrForwardImpl(steps); + } + + private void goBackOrForwardImpl(int steps) { goBackOrForward(steps, false); } @@ -2030,6 +2191,7 @@ public class WebView extends AbsoluteLayout * Returns true if private browsing is enabled in this WebView. */ public boolean isPrivateBrowsingEnabled() { + checkThread(); return getSettings().isPrivateBrowsingEnabled(); } @@ -2052,6 +2214,7 @@ public class WebView extends AbsoluteLayout * @return true if the page was scrolled */ public boolean pageUp(boolean top) { + checkThread(); if (mNativeClass == 0) { return false; } @@ -2078,6 +2241,7 @@ public class WebView extends AbsoluteLayout * @return true if the page was scrolled */ public boolean pageDown(boolean bottom) { + checkThread(); if (mNativeClass == 0) { return false; } @@ -2102,6 +2266,7 @@ public class WebView extends AbsoluteLayout * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY */ public void clearView() { + checkThread(); mContentWidth = 0; mContentHeight = 0; setBaseLayer(0, null, false, false); @@ -2118,6 +2283,7 @@ public class WebView extends AbsoluteLayout * bounds of the view. */ public Picture capturePicture() { + checkThread(); if (mNativeClass == 0) return null; Picture result = new Picture(); nativeCopyBaseContentToPicture(result); @@ -2148,6 +2314,7 @@ public class WebView extends AbsoluteLayout * @return The current scale. */ public float getScale() { + checkThread(); return mZoomManager.getScale(); } @@ -2160,6 +2327,7 @@ public class WebView extends AbsoluteLayout * @param scaleInPercent The initial scale in percent. */ public void setInitialScale(int scaleInPercent) { + checkThread(); mZoomManager.setInitialScaleInPercent(scaleInPercent); } @@ -2169,6 +2337,7 @@ public class WebView extends AbsoluteLayout * level of this WebView. */ public void invokeZoomPicker() { + checkThread(); if (!getSettings().supportZoom()) { Log.w(LOGTAG, "This WebView doesn't support zoom."); return; @@ -2196,6 +2365,7 @@ public class WebView extends AbsoluteLayout * HitTestResult type is set to UNKNOWN_TYPE. */ public HitTestResult getHitTestResult() { + checkThread(); return hitTestResult(mInitialHitTestResult); } @@ -2277,6 +2447,7 @@ public class WebView extends AbsoluteLayout * - "src" returns the image's src attribute. */ public void requestFocusNodeHref(Message hrefMsg) { + checkThread(); if (hrefMsg == null) { return; } @@ -2305,6 +2476,7 @@ public class WebView extends AbsoluteLayout * as the data member with "url" as key. The result can be null. */ public void requestImageRef(Message msg) { + checkThread(); if (0 == mNativeClass) return; // client isn't initialized int contentX = viewToContentX(mLastTouchX + mScrollX); int contentY = viewToContentY(mLastTouchY + mScrollY); @@ -2383,6 +2555,8 @@ public class WebView extends AbsoluteLayout */ public void setTitleBarGravity(int gravity) { mTitleGravity = gravity; + // force refresh + invalidate(); } /** @@ -2549,10 +2723,12 @@ public class WebView extends AbsoluteLayout calcOurContentVisibleRect(rect); // Rect.equals() checks for null input. if (!rect.equals(mLastVisibleRectSent)) { - Point pos = new Point(rect.left, rect.top); - mWebViewCore.removeMessages(EventHub.SET_SCROLL_OFFSET); - mWebViewCore.sendMessage(EventHub.SET_SCROLL_OFFSET, - nativeMoveGeneration(), mSendScrollEvent ? 1 : 0, pos); + if (!mBlockWebkitViewMessages) { + Point pos = new Point(rect.left, rect.top); + mWebViewCore.removeMessages(EventHub.SET_SCROLL_OFFSET); + mWebViewCore.sendMessage(EventHub.SET_SCROLL_OFFSET, + nativeMoveGeneration(), mSendScrollEvent ? 1 : 0, pos); + } mLastVisibleRectSent = rect; mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); } @@ -2567,7 +2743,9 @@ public class WebView extends AbsoluteLayout // TODO: the global offset is only used by windowRect() // in ChromeClientAndroid ; other clients such as touch // and mouse events could return view + screen relative points. - mWebViewCore.sendMessage(EventHub.SET_GLOBAL_BOUNDS, globalRect); + if (!mBlockWebkitViewMessages) { + mWebViewCore.sendMessage(EventHub.SET_GLOBAL_BOUNDS, globalRect); + } mLastGlobalRect = globalRect; } return rect; @@ -2632,6 +2810,7 @@ public class WebView extends AbsoluteLayout * @return true if new values were sent */ boolean sendViewSizeZoom(boolean force) { + if (mBlockWebkitViewMessages) return false; if (mZoomManager.isPreventingWebkitUpdates()) return false; int viewWidth = getViewWidth(); @@ -2799,6 +2978,7 @@ public class WebView extends AbsoluteLayout * @return The url for the current page. */ public String getUrl() { + checkThread(); WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getUrl() : null; } @@ -2812,6 +2992,7 @@ public class WebView extends AbsoluteLayout * @return The url that was originally requested for the current page. */ public String getOriginalUrl() { + checkThread(); WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getOriginalUrl() : null; } @@ -2822,6 +3003,7 @@ public class WebView extends AbsoluteLayout * @return The title for the current page. */ public String getTitle() { + checkThread(); WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getTitle() : null; } @@ -2832,6 +3014,7 @@ public class WebView extends AbsoluteLayout * @return The favicon for the current page. */ public Bitmap getFavicon() { + checkThread(); WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getFavicon() : null; } @@ -2852,6 +3035,7 @@ public class WebView extends AbsoluteLayout * @return The progress for the current page between 0 and 100. */ public int getProgress() { + checkThread(); return mCallbackProxy.getProgress(); } @@ -2859,6 +3043,7 @@ public class WebView extends AbsoluteLayout * @return the height of the HTML content. */ public int getContentHeight() { + checkThread(); return mContentHeight; } @@ -2876,6 +3061,7 @@ public class WebView extends AbsoluteLayout * useful if the application has been paused. */ public void pauseTimers() { + checkThread(); mWebViewCore.sendMessage(EventHub.PAUSE_TIMERS); } @@ -2884,6 +3070,7 @@ public class WebView extends AbsoluteLayout * This will resume dispatching all timers. */ public void resumeTimers() { + checkThread(); mWebViewCore.sendMessage(EventHub.RESUME_TIMERS); } @@ -2896,6 +3083,7 @@ public class WebView extends AbsoluteLayout * Note that this differs from pauseTimers(), which affects all WebViews. */ public void onPause() { + checkThread(); if (!mIsPaused) { mIsPaused = true; mWebViewCore.sendMessage(EventHub.ON_PAUSE); @@ -2911,6 +3099,7 @@ public class WebView extends AbsoluteLayout * Call this to resume a WebView after a previous call to onPause(). */ public void onResume() { + checkThread(); if (mIsPaused) { mIsPaused = false; mWebViewCore.sendMessage(EventHub.ON_RESUME); @@ -2931,6 +3120,7 @@ public class WebView extends AbsoluteLayout * free any available memory. */ public void freeMemory() { + checkThread(); mWebViewCore.sendMessage(EventHub.FREE_MEMORY); } @@ -2941,6 +3131,7 @@ public class WebView extends AbsoluteLayout * @param includeDiskFiles If false, only the RAM cache is cleared. */ public void clearCache(boolean includeDiskFiles) { + checkThread(); // Note: this really needs to be a static method as it clears cache for all // WebView. But we need mWebViewCore to send message to WebCore thread, so // we can't make this static. @@ -2953,6 +3144,7 @@ public class WebView extends AbsoluteLayout * currently focused textfield if there is one. */ public void clearFormData() { + checkThread(); if (inEditingMode()) { AutoCompleteAdapter adapter = null; mWebTextView.setAdapterCustom(adapter); @@ -2963,6 +3155,7 @@ public class WebView extends AbsoluteLayout * Tell the WebView to clear its internal back/forward list. */ public void clearHistory() { + checkThread(); mCallbackProxy.getBackForwardList().setClearPending(); mWebViewCore.sendMessage(EventHub.CLEAR_HISTORY); } @@ -2972,6 +3165,7 @@ public class WebView extends AbsoluteLayout * certificate errors. */ public void clearSslPreferences() { + checkThread(); mWebViewCore.sendMessage(EventHub.CLEAR_SSL_PREF_TABLE); } @@ -2984,6 +3178,7 @@ public class WebView extends AbsoluteLayout * updated to reflect any new state. */ public WebBackForwardList copyBackForwardList() { + checkThread(); return mCallbackProxy.getBackForwardList().clone(); } @@ -2995,6 +3190,7 @@ public class WebView extends AbsoluteLayout * @param forward Direction to search. */ public void findNext(boolean forward) { + checkThread(); if (0 == mNativeClass) return; // client isn't initialized nativeFindNext(forward); } @@ -3006,6 +3202,7 @@ public class WebView extends AbsoluteLayout * that were found. */ public int findAll(String find) { + checkThread(); if (0 == mNativeClass) return 0; // client isn't initialized int result = find != null ? nativeFindAll(find.toLowerCase(), find.toUpperCase(), find.equalsIgnoreCase(mLastFind)) : 0; @@ -3025,6 +3222,7 @@ public class WebView extends AbsoluteLayout * @return boolean True if the find dialog is shown, false otherwise. */ public boolean showFindDialog(String text, boolean showIme) { + checkThread(); FindActionModeCallback callback = new FindActionModeCallback(mContext); if (getParent() == null || startActionMode(callback) == null) { // Could not start the action mode, so end Find on page @@ -3101,6 +3299,7 @@ public class WebView extends AbsoluteLayout * @return the address, or if no address is found, return null. */ public static String findAddress(String addr) { + checkThread(); return findAddress(addr, false); } @@ -3134,6 +3333,7 @@ public class WebView extends AbsoluteLayout * Clear the highlighting surrounding text matches created by findAll. */ public void clearMatches() { + checkThread(); if (mNativeClass == 0) return; nativeSetFindIsEmpty(); @@ -3163,6 +3363,7 @@ public class WebView extends AbsoluteLayout * @param response The message that will be dispatched with the result. */ public void documentHasImages(Message response) { + checkThread(); if (response == null) { return; } @@ -3222,9 +3423,11 @@ public class WebView extends AbsoluteLayout } abortAnimation(); mPrivateHandler.removeMessages(RESUME_WEBCORE_PRIORITY); - WebViewCore.resumePriority(); - if (!mSelectingText) { - WebViewCore.resumeUpdatePicture(mWebViewCore); + if (!mBlockWebkitViewMessages) { + WebViewCore.resumePriority(); + if (!mSelectingText) { + WebViewCore.resumeUpdatePicture(mWebViewCore); + } } if (oldX != mScrollX || oldY != mScrollY) { sendOurVisibleRect(); @@ -3558,6 +3761,7 @@ public class WebView extends AbsoluteLayout * @param client An implementation of WebViewClient. */ public void setWebViewClient(WebViewClient client) { + checkThread(); mCallbackProxy.setWebViewClient(client); } @@ -3578,6 +3782,7 @@ public class WebView extends AbsoluteLayout * @param listener An implementation of DownloadListener. */ public void setDownloadListener(DownloadListener listener) { + checkThread(); mCallbackProxy.setDownloadListener(listener); } @@ -3588,6 +3793,7 @@ public class WebView extends AbsoluteLayout * @param client An implementation of WebChromeClient. */ public void setWebChromeClient(WebChromeClient client) { + checkThread(); mCallbackProxy.setWebChromeClient(client); } @@ -3628,6 +3834,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public void setPictureListener(PictureListener listener) { + checkThread(); mPictureListener = listener; } @@ -3669,6 +3876,7 @@ public class WebView extends AbsoluteLayout * JavaScript. */ public void addJavascriptInterface(Object obj, String interfaceName) { + checkThread(); if (obj == null) { return; } @@ -3683,6 +3891,7 @@ public class WebView extends AbsoluteLayout * @param interfaceName The name of the interface to remove. */ public void removeJavascriptInterface(String interfaceName) { + checkThread(); if (mWebViewCore != null) { WebViewCore.JSInterfaceData arg = new WebViewCore.JSInterfaceData(); arg.mInterfaceName = interfaceName; @@ -3697,6 +3906,7 @@ public class WebView extends AbsoluteLayout * settings. */ public WebSettings getSettings() { + checkThread(); return (mWebViewCore != null) ? mWebViewCore.getSettings() : null; } @@ -3709,6 +3919,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public static synchronized PluginList getPluginList() { + checkThread(); return new PluginList(); } @@ -3717,7 +3928,9 @@ public class WebView extends AbsoluteLayout * @deprecated This was used for Gears, which has been deprecated. */ @Deprecated - public void refreshPlugins(boolean reloadOpenPages) { } + public void refreshPlugins(boolean reloadOpenPages) { + checkThread(); + } //------------------------------------------------------------------------- // Override View methods @@ -3726,7 +3939,7 @@ public class WebView extends AbsoluteLayout @Override protected void finalize() throws Throwable { try { - destroy(); + destroyImpl(); } finally { super.finalize(); } @@ -4045,6 +4258,10 @@ public class WebView extends AbsoluteLayout } } + int getBaseLayer() { + return nativeGetBaseLayer(); + } + private void onZoomAnimationStart() { // If it is in password mode, turn it off so it does not draw misplaced. if (inEditingMode() && nativeFocusCandidateIsPassword()) { @@ -4068,7 +4285,7 @@ public class WebView extends AbsoluteLayout } void onFixedLengthZoomAnimationEnd() { - if (!mSelectingText) { + if (!mBlockWebkitViewMessages && !mSelectingText) { WebViewCore.resumeUpdatePicture(mWebViewCore); } onZoomAnimationEnd(); @@ -4169,7 +4386,7 @@ public class WebView extends AbsoluteLayout // synchronization problem with layers. int content = nativeDraw(canvas, color, extras, false); canvas.setDrawFilter(null); - if (content != 0) { + if (!mBlockWebkitViewMessages && content != 0) { mWebViewCore.sendMessage(EventHub.SPLIT_PICTURE_SET, content, 0); } } @@ -4225,15 +4442,20 @@ public class WebView extends AbsoluteLayout } WebViewCore.CursorData cursorData() { - WebViewCore.CursorData result = new WebViewCore.CursorData(); - result.mMoveGeneration = nativeMoveGeneration(); - result.mFrame = nativeCursorFramePointer(); + WebViewCore.CursorData result = cursorDataNoPosition(); Point position = nativeCursorPosition(); result.mX = position.x; result.mY = position.y; return result; } + WebViewCore.CursorData cursorDataNoPosition() { + WebViewCore.CursorData result = new WebViewCore.CursorData(); + result.mMoveGeneration = nativeMoveGeneration(); + result.mFrame = nativeCursorFramePointer(); + return result; + } + /** * Delete text from start to end in the focused textfield. If there is no * focus, or if start == end, silently fail. If start and end are out of @@ -4568,6 +4790,9 @@ public class WebView extends AbsoluteLayout @Override public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) { + if (mBlockWebkitViewMessages) { + return false; + } // send complex characters to webkit for use by JS and plugins if (keyCode == KeyEvent.KEYCODE_UNKNOWN && event.getCharacters() != null) { // pass the key to DOM @@ -4592,6 +4817,9 @@ public class WebView extends AbsoluteLayout + "keyCode=" + keyCode + ", " + event + ", unicode=" + event.getUnicodeChar()); } + if (mBlockWebkitViewMessages) { + return false; + } // don't implement accelerator keys here; defer to host application if (event.isCtrlPressed()) { @@ -4795,6 +5023,9 @@ public class WebView extends AbsoluteLayout Log.v(LOGTAG, "keyUp at " + System.currentTimeMillis() + ", " + event + ", unicode=" + event.getUnicodeChar()); } + if (mBlockWebkitViewMessages) { + return false; + } if (mNativeClass == 0) { return false; @@ -4989,6 +5220,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public void emulateShiftHeld() { + checkThread(); setUpSelect(false, 0, 0); } @@ -5305,6 +5537,13 @@ public class WebView extends AbsoluteLayout } mZoomManager.onSizeChanged(w, h, ow, oh); + + if (mLoadedPicture != null && mDelaySetPicture == null) { + // Size changes normally result in a new picture + // Re-set the loaded picture to simulate that + // However, do not update the base layer as that hasn't changed + setNewPicture(mLoadedPicture, false); + } } @Override @@ -5378,10 +5617,12 @@ public class WebView extends AbsoluteLayout } private boolean shouldForwardTouchEvent() { - return mFullScreenHolder != null || (mForwardTouchEvents + if (mFullScreenHolder != null) return true; + if (mBlockWebkitViewMessages) return false; + return mForwardTouchEvents && !mSelectingText && mPreventDefault != PREVENT_DEFAULT_IGNORE - && mPreventDefault != PREVENT_DEFAULT_NO); + && mPreventDefault != PREVENT_DEFAULT_NO; } private boolean inFullScreenMode() { @@ -5390,7 +5631,7 @@ public class WebView extends AbsoluteLayout private void dismissFullScreenMode() { if (inFullScreenMode()) { - mFullScreenHolder.dismiss(); + mFullScreenHolder.hide(); mFullScreenHolder = null; } } @@ -5432,6 +5673,18 @@ public class WebView extends AbsoluteLayout private static final int DRAG_LAYER_FINGER_DISTANCE = 20000; @Override + public boolean onHoverEvent(MotionEvent event) { + if (mNativeClass == 0) { + return false; + } + WebViewCore.CursorData data = cursorDataNoPosition(); + data.mX = viewToContentX((int) event.getX()); + data.mY = viewToContentY((int) event.getY()); + mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, data); + return true; + } + + @Override public boolean onTouchEvent(MotionEvent ev) { if (mNativeClass == 0 || (!isClickable() && !isLongClickable())) { return false; @@ -5500,25 +5753,31 @@ public class WebView extends AbsoluteLayout // commit the short press action for the previous tap doShortPress(); mTouchMode = TOUCH_INIT_MODE; - mDeferTouchProcess = (!inFullScreenMode() - && mForwardTouchEvents) ? hitFocusedPlugin( - contentX, contentY) : false; + mDeferTouchProcess = !mBlockWebkitViewMessages + && (!inFullScreenMode() && mForwardTouchEvents) + ? hitFocusedPlugin(contentX, contentY) + : false; } } else { // the normal case mTouchMode = TOUCH_INIT_MODE; - mDeferTouchProcess = (!inFullScreenMode() - && mForwardTouchEvents) ? hitFocusedPlugin( - contentX, contentY) : false; - mWebViewCore.sendMessage( - EventHub.UPDATE_FRAME_CACHE_IF_LOADING); + mDeferTouchProcess = !mBlockWebkitViewMessages + && (!inFullScreenMode() && mForwardTouchEvents) + ? hitFocusedPlugin(contentX, contentY) + : false; + if (!mBlockWebkitViewMessages) { + mWebViewCore.sendMessage( + EventHub.UPDATE_FRAME_CACHE_IF_LOADING); + } if (getSettings().supportTouchOnly()) { TouchHighlightData data = new TouchHighlightData(); data.mX = contentX; data.mY = contentY; data.mSlop = viewToContentDimension(mNavSlop); - mWebViewCore.sendMessageDelayed( - EventHub.GET_TOUCH_HIGHLIGHT_RECTS, data, - ViewConfiguration.getTapTimeout()); + if (!mBlockWebkitViewMessages) { + mWebViewCore.sendMessageDelayed( + EventHub.GET_TOUCH_HIGHLIGHT_RECTS, data, + ViewConfiguration.getTapTimeout()); + } if (DEBUG_TOUCH_HIGHLIGHT) { if (getSettings().getNavDump()) { mTouchHighlightX = (int) x + mScrollX; @@ -5554,7 +5813,7 @@ public class WebView extends AbsoluteLayout SWITCH_TO_LONGPRESS, LONG_PRESS_TIMEOUT); if (inFullScreenMode() || mDeferTouchProcess) { mPreventDefault = PREVENT_DEFAULT_YES; - } else if (mForwardTouchEvents) { + } else if (!mBlockWebkitViewMessages && mForwardTouchEvents) { mPreventDefault = PREVENT_DEFAULT_MAYBE_YES; } else { mPreventDefault = PREVENT_DEFAULT_NO; @@ -6257,7 +6516,11 @@ public class WebView extends AbsoluteLayout // arrow key events, not trackball events, from one child to the next private boolean mMapTrackballToArrowKeys = true; + private DrawData mDelaySetPicture; + private DrawData mLoadedPicture; + public void setMapTrackballToArrowKeys(boolean setMap) { + checkThread(); mMapTrackballToArrowKeys = setMap; } @@ -6550,6 +6813,7 @@ public class WebView extends AbsoluteLayout } public void flingScroll(int vx, int vy) { + checkThread(); mScroller.fling(mScrollX, mScrollY, vx, vy, 0, computeMaxScrollX(), 0, computeMaxScrollY(), mOverflingDistance, mOverflingDistance); invalidate(); @@ -6588,11 +6852,6 @@ public class WebView extends AbsoluteLayout vx = 0; } } - if (true /* EMG release: make our fling more like Maps' */) { - // maps cuts their velocity in half - vx = vx * 3 / 4; - vy = vy * 3 / 4; - } if ((maxX == 0 && vy == 0) || (maxY == 0 && vx == 0)) { WebViewCore.resumePriority(); if (!mSelectingText) { @@ -6685,6 +6944,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public View getZoomControls() { + checkThread(); if (!getSettings().supportZoom()) { Log.w(LOGTAG, "This WebView doesn't support zoom."); return null; @@ -6704,6 +6964,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if the WebView can be zoomed in. */ public boolean canZoomIn() { + checkThread(); return mZoomManager.canZoomIn(); } @@ -6711,6 +6972,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if the WebView can be zoomed out. */ public boolean canZoomOut() { + checkThread(); return mZoomManager.canZoomOut(); } @@ -6719,6 +6981,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if zoom in succeeds. FALSE if no zoom changes. */ public boolean zoomIn() { + checkThread(); return mZoomManager.zoomIn(); } @@ -6727,6 +6990,7 @@ public class WebView extends AbsoluteLayout * @return TRUE if zoom out succeeds. FALSE if no zoom changes. */ public boolean zoomOut() { + checkThread(); return mZoomManager.zoomOut(); } @@ -7027,7 +7291,6 @@ public class WebView extends AbsoluteLayout if (measuredHeight > heightSize) { measuredHeight = heightSize; mHeightCanMeasure = false; - } else if (measuredHeight < heightSize) { measuredHeight |= MEASURED_STATE_TOO_SMALL; } } @@ -7144,7 +7407,10 @@ public class WebView extends AbsoluteLayout cursorData(), 1000); } - /* package */ synchronized WebViewCore getWebViewCore() { + /** + * @hide + */ + public synchronized WebViewCore getWebViewCore() { return mWebViewCore; } @@ -7656,6 +7922,11 @@ public class WebView extends AbsoluteLayout // after WebView's destroy() is called, skip handling messages. return; } + if (mBlockWebkitViewMessages + && msg.what != WEBCORE_INITIALIZED_MSG_ID) { + // Blocking messages from webkit + return; + } switch (msg.what) { case REMEMBER_PASSWORD: { mDatabase.setUsernamePassword( @@ -7791,71 +8062,19 @@ public class WebView extends AbsoluteLayout case NEW_PICTURE_MSG_ID: { // called for new content final WebViewCore.DrawData draw = (WebViewCore.DrawData) msg.obj; - WebViewCore.ViewState viewState = draw.mViewState; - boolean isPictureAfterFirstLayout = viewState != null; - setBaseLayer(draw.mBaseLayer, draw.mInvalRegion, - getSettings().getShowVisualIndicator(), - isPictureAfterFirstLayout); - final Point viewSize = draw.mViewSize; - if (isPictureAfterFirstLayout) { - // Reset the last sent data here since dealing with new page. - mLastWidthSent = 0; - mZoomManager.onFirstLayout(draw); - if (!mDrawHistory) { - // Do not send the scroll event for this particular - // scroll message. Note that a scroll event may - // still be fired if the user scrolls before the - // message can be handled. - mSendScrollEvent = false; - setContentScrollTo(viewState.mScrollX, viewState.mScrollY); - mSendScrollEvent = true; - - // As we are on a new page, remove the WebTextView. This - // is necessary for page loads driven by webkit, and in - // particular when the user was on a password field, so - // the WebTextView was visible. - clearTextEntry(); - } - } - - // We update the layout (i.e. request a layout from the - // view system) if the last view size that we sent to - // WebCore matches the view size of the picture we just - // received in the fixed dimension. - final boolean updateLayout = viewSize.x == mLastWidthSent - && viewSize.y == mLastHeightSent; - // Don't send scroll event for picture coming from webkit, - // since the new picture may cause a scroll event to override - // the saved history scroll position. - mSendScrollEvent = false; - recordNewContentSize(draw.mContentSize.x, - draw.mContentSize.y, updateLayout); - mSendScrollEvent = true; - if (DebugFlags.WEB_VIEW) { - Rect b = draw.mInvalRegion.getBounds(); - Log.v(LOGTAG, "NEW_PICTURE_MSG_ID {" + - b.left+","+b.top+","+b.right+","+b.bottom+"}"); - } - invalidateContentRect(draw.mInvalRegion.getBounds()); - - if (mPictureListener != null) { - mPictureListener.onNewPicture(WebView.this, capturePicture()); - } - - // update the zoom information based on the new picture - mZoomManager.onNewPicture(draw); - - if (draw.mFocusSizeChanged && inEditingMode()) { - mFocusSizeChanged = true; - } - if (isPictureAfterFirstLayout) { - mViewManager.postReadyToDrawAll(); - } + setNewPicture(draw, true); break; } 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); + if (mDelaySetPicture != null) { + setNewPicture(mDelaySetPicture, true); + mDelaySetPicture = null; + } break; case UPDATE_TEXTFIELD_TEXT_MSG_ID: // Make sure that the textfield is currently focused @@ -8020,16 +8239,15 @@ public class WebView extends AbsoluteLayout case SHOW_FULLSCREEN: { View view = (View) msg.obj; - int npp = msg.arg1; + int orientation = msg.arg1; + int npp = msg.arg2; if (inFullScreenMode()) { Log.w(LOGTAG, "Should not have another full screen."); dismissFullScreenMode(); } - mFullScreenHolder = new PluginFullScreenHolder(WebView.this, npp); + mFullScreenHolder = new PluginFullScreenHolder(WebView.this, orientation, npp); mFullScreenHolder.setContentView(view); - mFullScreenHolder.setCancelable(false); - mFullScreenHolder.setCanceledOnTouchOutside(false); mFullScreenHolder.show(); break; @@ -8167,6 +8385,80 @@ public class WebView extends AbsoluteLayout } } + void setNewPicture(final WebViewCore.DrawData draw, boolean updateBaseLayer) { + if (mNativeClass == 0) { + if (mDelaySetPicture != null) { + throw new IllegalStateException("Tried to setNewPicture with" + + " a delay picture already set! (memory leak)"); + } + // Not initialized yet, delay set + mDelaySetPicture = draw; + return; + } + WebViewCore.ViewState viewState = draw.mViewState; + boolean isPictureAfterFirstLayout = viewState != null; + if (updateBaseLayer) { + setBaseLayer(draw.mBaseLayer, draw.mInvalRegion, + getSettings().getShowVisualIndicator(), + isPictureAfterFirstLayout); + } + final Point viewSize = draw.mViewSize; + if (isPictureAfterFirstLayout) { + // Reset the last sent data here since dealing with new page. + mLastWidthSent = 0; + mZoomManager.onFirstLayout(draw); + if (!mDrawHistory) { + // Do not send the scroll event for this particular + // scroll message. Note that a scroll event may + // still be fired if the user scrolls before the + // message can be handled. + mSendScrollEvent = false; + setContentScrollTo(viewState.mScrollX, viewState.mScrollY); + mSendScrollEvent = true; + + // As we are on a new page, remove the WebTextView. This + // is necessary for page loads driven by webkit, and in + // particular when the user was on a password field, so + // the WebTextView was visible. + clearTextEntry(); + } + } + + // We update the layout (i.e. request a layout from the + // view system) if the last view size that we sent to + // WebCore matches the view size of the picture we just + // received in the fixed dimension. + final boolean updateLayout = viewSize.x == mLastWidthSent + && viewSize.y == mLastHeightSent; + // Don't send scroll event for picture coming from webkit, + // since the new picture may cause a scroll event to override + // the saved history scroll position. + mSendScrollEvent = false; + recordNewContentSize(draw.mContentSize.x, + draw.mContentSize.y, updateLayout); + mSendScrollEvent = true; + if (DebugFlags.WEB_VIEW) { + Rect b = draw.mInvalRegion.getBounds(); + Log.v(LOGTAG, "NEW_PICTURE_MSG_ID {" + + b.left+","+b.top+","+b.right+","+b.bottom+"}"); + } + invalidateContentRect(draw.mInvalRegion.getBounds()); + + if (mPictureListener != null) { + mPictureListener.onNewPicture(WebView.this, capturePicture()); + } + + // update the zoom information based on the new picture + mZoomManager.onNewPicture(draw); + + if (draw.mFocusSizeChanged && inEditingMode()) { + mFocusSizeChanged = true; + } + if (isPictureAfterFirstLayout) { + mViewManager.postReadyToDrawAll(); + } + } + /** * Used when receiving messages for REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID * and UPDATE_TEXT_SELECTION_MSG_ID. Update the selection of WebTextView. @@ -8684,6 +8976,7 @@ public class WebView extends AbsoluteLayout */ @Deprecated public void debugDump() { + checkThread(); nativeDebugDump(); mWebViewCore.sendMessage(EventHub.DUMP_NAVTREE); } @@ -8745,12 +9038,24 @@ public class WebView extends AbsoluteLayout return mViewManager; } + private static void checkThread() { + if (Looper.myLooper() != Looper.getMainLooper()) { + RuntimeException exception = new RuntimeException( + "A WebView method was called on thread '" + + Thread.currentThread().getName() + "'. " + + "All WebView methods must be called on the UI thread. " + + "Future versions of WebView may not support use on other threads."); + Log.e(LOGTAG, Log.getStackTraceString(exception)); + StrictMode.onWebViewMethodCalledOnWrongThread(exception); + } + } + private native int nativeCacheHitFramePointer(); private native boolean nativeCacheHitIsPlugin(); private native Rect nativeCacheHitNodeBounds(); private native int nativeCacheHitNodePointer(); /* package */ native void nativeClearCursor(); - private native void nativeCreate(int ptr); + private native void nativeCreate(int ptr, String drawableDir, AssetManager am); private native int nativeCursorFramePointer(); private native Rect nativeCursorNodeBounds(); private native int nativeCursorNodePointer(); @@ -8856,6 +9161,7 @@ public class WebView extends AbsoluteLayout private native void nativeSetHeightCanMeasure(boolean measure); private native void nativeSetBaseLayer(int layer, Region invalRegion, boolean showVisualIndicator, boolean isPictureAfterFirstLayout); + private native int nativeGetBaseLayer(); private native void nativeShowCursorTimed(); private native void nativeReplaceBaseContent(int content); private native void nativeCopyBaseContentToPicture(Picture pict); diff --git a/core/java/android/webkit/WebViewClient.java b/core/java/android/webkit/WebViewClient.java index 65026a5..d3be2bf 100644 --- a/core/java/android/webkit/WebViewClient.java +++ b/core/java/android/webkit/WebViewClient.java @@ -30,7 +30,7 @@ public class WebViewClient { * proper handler for the url. If WebViewClient is provided, return true * means the host application handles the url, while return false means the * current WebView handles the url. - * + * * @param view The WebView that is initiating the callback. * @param url The url to be loaded. * @return True if the host application wants to leave the current WebView @@ -46,7 +46,7 @@ public class WebViewClient { * framesets will call onPageStarted one time for the main frame. This also * means that onPageStarted will not be called when the contents of an * embedded frame changes, i.e. clicking a link whose target is an iframe. - * + * * @param view The WebView that is initiating the callback. * @param url The url to be loaded. * @param favicon The favicon for this page if it already exists in the @@ -60,7 +60,7 @@ public class WebViewClient { * is called only for main frame. When onPageFinished() is called, the * rendering picture may not be updated yet. To get the notification for the * new Picture, use {@link WebView.PictureListener#onNewPicture}. - * + * * @param view The WebView that is initiating the callback. * @param url The url of the page. */ @@ -70,7 +70,7 @@ public class WebViewClient { /** * Notify the host application that the WebView will load the resource * specified by the given url. - * + * * @param view The WebView that is initiating the callback. * @param url The url of the resource the WebView will load. */ @@ -102,7 +102,7 @@ public class WebViewClient { * HTTP redirects. As the host application if it would like to continue * trying to load the resource. The default behavior is to send the cancel * message. - * + * * @param view The WebView that is initiating the callback. * @param cancelMsg The message to send if the host wants to cancel * @param continueMsg The message to send if the host wants to continue @@ -164,7 +164,7 @@ public class WebViewClient { * As the host application if the browser should resend data as the * requested page was a result of a POST. The default is to not resend the * data. - * + * * @param view The WebView that is initiating the callback. * @param dontResend The message to send if the browser should not resend * @param resend The message to send if the browser should resend data @@ -176,7 +176,7 @@ public class WebViewClient { /** * Notify the host application to update its visited links database. - * + * * @param view The WebView that is initiating the callback. * @param url The url being visited. * @param isReload True if this url is being reloaded. @@ -186,12 +186,12 @@ public class WebViewClient { } /** - * Notify the host application to handle a ssl certificate error request + * Notify the host application to handle a SSL certificate error request * (display the error to the user and ask whether to proceed or not). The * host application has to call either handler.cancel() or handler.proceed() * as the connection is suspended and waiting for the response. The default * behavior is to cancel the load. - * + * * @param view The WebView that is initiating the callback. * @param handler An SslErrorHandler object that will handle the user's * response. @@ -203,9 +203,29 @@ public class WebViewClient { } /** + * Notify the host application to handle a SSL client certificate + * request (display the request to the user and ask whether to + * proceed with a client certificate or not). The host application + * has to call either handler.cancel() or handler.proceed() as the + * connection is suspended and waiting for the response. The + * default behavior is to cancel, returning no client certificate. + * + * @param view The WebView that is initiating the callback. + * @param handler An ClientCertRequestHandler object that will + * handle the user's response. + * @param host_and_port The host and port of the requesting server. + * + * @hide + */ + public void onReceivedClientCertRequest(WebView view, + ClientCertRequestHandler handler, String host_and_port) { + handler.cancel(); + } + + /** * Notify the host application to handle an authentication request. The * default behavior is to cancel the request. - * + * * @param view The WebView that is initiating the callback. * @param handler The HttpAuthHandler that will handle the user's response. * @param host The host requiring authentication. @@ -223,7 +243,7 @@ public class WebViewClient { * true, WebView will not handle the key event. If return false, WebView * will always handle the key event, so none of the super in the view chain * will see the key event. The default behavior returns false. - * + * * @param view The WebView that is initiating the callback. * @param event The key event. * @return True if the host application wants to handle the key event @@ -239,7 +259,7 @@ public class WebViewClient { * or if shouldOverrideKeyEvent returns true. This is called asynchronously * from where the key is dispatched. It gives the host application an chance * to handle the unhandled key events. - * + * * @param view The WebView that is initiating the callback. * @param event The key event. */ @@ -249,7 +269,7 @@ public class WebViewClient { /** * Notify the host application that the scale applied to the WebView has * changed. - * + * * @param view he WebView that is initiating the callback. * @param oldScale The old scale factor * @param newScale The new scale factor diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index e73c9d0..06a61bd 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -49,7 +49,10 @@ import java.util.Set; import junit.framework.Assert; -final class WebViewCore { +/** + * @hide + */ +public final class WebViewCore { private static final String LOGTAG = "webcore"; @@ -906,7 +909,10 @@ final class WebViewCore { "REMOVE_JS_INTERFACE", // = 149; }; - class EventHub { + /** + * @hide + */ + public class EventHub { // Message Ids static final int REVEAL_SELECTION = 96; static final int REQUEST_LABEL = 97; @@ -937,7 +943,7 @@ final class WebViewCore { static final int DELETE_SELECTION = 122; static final int LISTBOX_CHOICES = 123; static final int SINGLE_LISTBOX_CHOICE = 124; - static final int MESSAGE_RELAY = 125; + public static final int MESSAGE_RELAY = 125; static final int SET_BACKGROUND_COLOR = 126; static final int SET_MOVE_FOCUS = 127; static final int SAVE_DOCUMENT_STATE = 128; @@ -1135,16 +1141,13 @@ final class WebViewCore { if (baseUrl != null) { int i = baseUrl.indexOf(':'); if (i > 0) { - /* - * In 1.0, {@link - * WebView#loadDataWithBaseURL} can access - * local asset files as long as the data is - * valid. In the new WebKit, the restriction - * is tightened. To be compatible with 1.0, - * we automatically add the scheme of the - * baseUrl for local access as long as it is - * not http(s)/ftp(s)/about/javascript - */ + // In 1.0, WebView.loadDataWithBaseURL() could access local + // asset files using 'file' scheme URLs as long as the data is + // valid. Later versions of WebKit have tightened the + // restriction around when pages can access such local URLs. + // To maintain compatibility with 1.0, we register the scheme of + // the baseUrl to be considered local, as long as it is not + // http(s)/ftp(s)/about/javascript. String scheme = baseUrl.substring(0, i); if (!scheme.startsWith("http") && !scheme.startsWith("ftp") && @@ -1703,7 +1706,10 @@ final class WebViewCore { // If it needs WebCore, it has to send message. //------------------------------------------------------------------------- - void sendMessage(Message msg) { + /** + * @hide + */ + public void sendMessage(Message msg) { mEventHub.sendMessage(msg); } @@ -1857,7 +1863,7 @@ final class WebViewCore { Log.w(LOGTAG, "skip viewSizeChanged as w is 0"); return; } - int width = calculateWindowWidth(w, textwrapWidth); + int width = calculateWindowWidth(w); int height = h; if (width != w) { float heightWidthRatio = data.mHeightWidthRatio; @@ -1883,41 +1889,18 @@ final class WebViewCore { } // Calculate width to be used in webkit window. - private int calculateWindowWidth(int viewWidth, int textwrapWidth) { + private int calculateWindowWidth(int viewWidth) { int width = viewWidth; if (mSettings.getUseWideViewPort()) { if (mViewportWidth == -1) { - if (mSettings.getLayoutAlgorithm() == - WebSettings.LayoutAlgorithm.NORMAL || mSettings.getUseFixedViewport()) { - width = WebView.DEFAULT_VIEWPORT_WIDTH; - } else { - /* - * if a page's minimum preferred width is wider than the - * given "w", use it instead to get better layout result. If - * we start a page with MAX_ZOOM_WIDTH, "w" will be always - * wider. If we start a page with screen width, due to the - * delay between {@link #didFirstLayout} and - * {@link #viewSizeChanged}, - * {@link #nativeGetContentMinPrefWidth} will return a more - * accurate value than initial 0 to result a better layout. - * In the worse case, the native width will be adjusted when - * next zoom or screen orientation change happens. - */ - width = Math.min(WebView.sMaxViewportWidth, Math.max(viewWidth, - Math.max(WebView.DEFAULT_VIEWPORT_WIDTH, - nativeGetContentMinPrefWidth()))); - } + // Fixed viewport width. + width = WebView.DEFAULT_VIEWPORT_WIDTH; } else if (mViewportWidth > 0) { - if (mSettings.getUseFixedViewport()) { - // Use website specified or desired fixed viewport width. - width = mViewportWidth; - } else { - width = Math.max(viewWidth, mViewportWidth); - } - } else if (mSettings.getUseFixedViewport()) { - width = mWebView.getViewWidth(); + // Use website specified or desired fixed viewport width. + width = mViewportWidth; } else { - width = textwrapWidth; + // For mobile web site. + width = Math.round(mWebView.getViewWidth() / mWebView.getDefaultZoomScale()); } } return width; @@ -2255,6 +2238,27 @@ final class WebViewCore { // set the viewport settings from WebKit setViewportSettingsFromNative(); + if (mSettings.forceUserScalable()) { + mViewportUserScalable = true; + if (mViewportInitialScale > 0) { + if (mViewportMinimumScale > 0) { + mViewportMinimumScale = Math.min(mViewportMinimumScale, + mViewportInitialScale / 2); + } + if (mViewportMaximumScale > 0) { + mViewportMaximumScale = Math.max(mViewportMaximumScale, + mViewportInitialScale * 2); + } + } else { + if (mViewportMinimumScale > 0) { + mViewportMinimumScale = Math.min(mViewportMinimumScale, 50); + } + if (mViewportMaximumScale > 0) { + mViewportMaximumScale = Math.max(mViewportMaximumScale, 200); + } + } + } + // adjust the default scale to match the densityDpi float adjust = 1.0f; if (mViewportDensityDpi == -1) { @@ -2411,8 +2415,7 @@ final class WebViewCore { // in zoom overview mode. tentativeScale = mInitialViewState.mTextWrapScale; int tentativeViewWidth = Math.round(webViewWidth / tentativeScale); - int windowWidth = calculateWindowWidth(tentativeViewWidth, - tentativeViewWidth); + int windowWidth = calculateWindowWidth(tentativeViewWidth); // In viewport setup time, since no content width is known, we assume // the windowWidth will be the content width, to get a more likely // zoom overview scale. @@ -2421,8 +2424,7 @@ final class WebViewCore { // If user choose non-overview mode. data.mScale = Math.max(data.mScale, tentativeScale); } - if (mSettings.isNarrowColumnLayout() && - mSettings.getUseFixedViewport()) { + if (mSettings.isNarrowColumnLayout()) { // In case of automatic text reflow in fixed view port mode. mInitialViewState.mTextWrapScale = ZoomManager.computeReadingLevelScale(data.mScale); @@ -2591,11 +2593,11 @@ final class WebViewCore { // called by JNI private Class<?> getPluginClass(String libName, String clsName) { - + if (mWebView == null) { return null; } - + PluginManager pluginManager = PluginManager.getInstance(null); String pkgName = pluginManager.getPluginsAPKName(libName); @@ -2603,7 +2605,7 @@ final class WebViewCore { Log.w(LOGTAG, "Unable to resolve " + libName + " to a plugin APK"); return null; } - + try { return pluginManager.getPluginClass(pkgName, clsName); } catch (NameNotFoundException e) { @@ -2618,14 +2620,15 @@ final class WebViewCore { // called by JNI. PluginWidget function to launch a full-screen view using a // View object provided by the plugin class. - private void showFullScreenPlugin(ViewManager.ChildView childView, int npp) { + private void showFullScreenPlugin(ViewManager.ChildView childView, int orientation, int npp) { if (mWebView == null) { return; } Message message = mWebView.mPrivateHandler.obtainMessage(WebView.SHOW_FULLSCREEN); message.obj = childView.mView; - message.arg1 = npp; + message.arg1 = orientation; + message.arg2 = npp; message.sendToTarget(); } @@ -2658,7 +2661,7 @@ final class WebViewCore { view.mView = pluginView; return view; } - + // called by JNI. PluginWidget functions for creating an embedded View for // the surface drawing model. private ViewManager.ChildView addSurface(View pluginView, int x, int y, diff --git a/core/java/android/webkit/ZoomManager.java b/core/java/android/webkit/ZoomManager.java index f2a1ec3..fe6fb2f 100644 --- a/core/java/android/webkit/ZoomManager.java +++ b/core/java/android/webkit/ZoomManager.java @@ -634,8 +634,17 @@ class ZoomManager { } else { newTextWrapScale = mActualScale; } + final boolean firstTimeReflow = !exceedsMinScaleIncrement(mActualScale, mTextWrapScale); + if (firstTimeReflow || mInZoomOverview) { + // In case first time reflow or in zoom overview mode, let reflow and zoom + // happen at the same time. + mTextWrapScale = newTextWrapScale; + } if (settings.isNarrowColumnLayout() - && exceedsMinScaleIncrement(mTextWrapScale, newTextWrapScale)) { + && exceedsMinScaleIncrement(mTextWrapScale, newTextWrapScale) + && !firstTimeReflow + && !mInZoomOverview) { + // Reflow only. mTextWrapScale = newTextWrapScale; refreshZoomScale(true); } else if (!mInZoomOverview && willScaleTriggerZoom(getZoomOverviewScale())) { @@ -906,7 +915,7 @@ class ZoomManager { // scaleAll(), we need to post a Runnable to ensure requestLayout(). // Additionally, only update the text wrap scale if the width changed. mWebView.post(new PostScale(w != ow && - !mWebView.getSettings().getUseFixedViewport(), mInZoomOverview)); + !mWebView.getSettings().getUseFixedViewport(), mInZoomOverview, w < ow)); } private class PostScale implements Runnable { @@ -915,10 +924,14 @@ class ZoomManager { // it could be changed between the time this callback is initiated and // the time it's actually run. final boolean mInZoomOverviewBeforeSizeChange; + final boolean mInPortraitMode; - public PostScale(boolean updateTextWrap, boolean inZoomOverview) { + public PostScale(boolean updateTextWrap, + boolean inZoomOverview, + boolean inPortraitMode) { mUpdateTextWrap = updateTextWrap; mInZoomOverviewBeforeSizeChange = inZoomOverview; + mInPortraitMode = inPortraitMode; } public void run() { @@ -926,9 +939,12 @@ class ZoomManager { // we always force, in case our height changed, in which case we // still want to send the notification over to webkit. // Keep overview mode unchanged when rotating. - final float zoomOverviewScale = getZoomOverviewScale(); - final float newScale = (mInZoomOverviewBeforeSizeChange) ? - zoomOverviewScale : Math.max(mActualScale, zoomOverviewScale); + float newScale = mActualScale; + if (mWebView.getSettings().getUseWideViewPort() && + mInPortraitMode && + mInZoomOverviewBeforeSizeChange) { + newScale = getZoomOverviewScale(); + } setZoomScale(newScale, mUpdateTextWrap, true); // update the zoom buttons as the scale can be changed updateZoomPicker(); @@ -999,8 +1015,9 @@ class ZoomManager { boolean mobileSiteInOverview = mInZoomOverview && !exceedsMinScaleIncrement(newZoomOverviewScale, 1.0f); if (!mWebView.drawHistory() && - (mInitialZoomOverview || scaleLessThanOverview || mobileSiteInOverview) && - scaleHasDiff && zoomOverviewWidthChanged) { + (scaleLessThanOverview || + ((mInitialZoomOverview || mobileSiteInOverview) && + scaleHasDiff && zoomOverviewWidthChanged))) { mInitialZoomOverview = false; setZoomScale(newZoomOverviewScale, !willScaleTriggerZoom(mTextWrapScale) && !mWebView.getSettings().getUseFixedViewport()); @@ -1019,23 +1036,15 @@ class ZoomManager { WebSettings settings = mWebView.getSettings(); int newZoomOverviewWidth = mZoomOverviewWidth; if (settings.getUseWideViewPort()) { - if (!settings.getUseFixedViewport()) { - // limit mZoomOverviewWidth upper bound to - // sMaxViewportWidth so that if the page doesn't behave - // well, the WebView won't go insane. limit the lower - // bound to match the default scale for mobile sites. - newZoomOverviewWidth = Math.min(WebView.sMaxViewportWidth, - Math.max((int) (viewWidth * mInvDefaultScale), - Math.max(drawData.mMinPrefWidth, drawData.mViewSize.x))); - } else if (drawData.mContentSize.x > 0) { + if (drawData.mContentSize.x > 0) { // The webkitDraw for layers will not populate contentSize, and it'll be // ignored for zoom overview width update. - final int contentWidth = Math.max(drawData.mContentSize.x, drawData.mMinPrefWidth); - newZoomOverviewWidth = Math.min(WebView.sMaxViewportWidth, contentWidth); + newZoomOverviewWidth = Math.min(WebView.sMaxViewportWidth, + drawData.mContentSize.x); } } else { // If not use wide viewport, use view width as the zoom overview width. - newZoomOverviewWidth = viewWidth; + newZoomOverviewWidth = Math.round(viewWidth / mDefaultScale); } if (newZoomOverviewWidth != mZoomOverviewWidth) { setZoomOverviewWidth(newZoomOverviewWidth); diff --git a/core/java/android/webkit/webdriver/By.java b/core/java/android/webkit/webdriver/By.java new file mode 100644 index 0000000..fa4fe74 --- /dev/null +++ b/core/java/android/webkit/webdriver/By.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +import java.util.List; + +/** + * Mechanism to locate elements within the DOM of the page. + * @hide + */ +public abstract class By { + public abstract WebElement findElement(WebElement element); + public abstract List<WebElement> findElements(WebElement element); + + /** + * Locates an element by its HTML id attribute. + * + * @param id The HTML id attribute to look for. + * @return A By instance that locates elements by their HTML id attributes. + */ + public static By id(final String id) { + throwIfNull(id); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementById(id); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsById(id); // Yes, it happens a lot. + } + + @Override + public String toString() { + return "By.id: " + id; + } + }; + } + + /** + * Locates an element by the matching the exact text on the HTML link. + * + * @param linkText The exact text to match against. + * @return A By instance that locates elements by the text displayed by + * the link. + */ + public static By linkText(final String linkText) { + throwIfNull(linkText); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByLinkText(linkText); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByLinkText(linkText); + } + + @Override + public String toString() { + return "By.linkText: " + linkText; + } + }; + } + + /** + * Locates an element by matching partial part of the text displayed by an + * HTML link. + * + * @param linkText The text that should be contained by the text displayed + * on the link. + * @return A By instance that locates elements that contain the given link + * text. + */ + public static By partialLinkText(final String linkText) { + throwIfNull(linkText); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByPartialLinkText(linkText); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByPartialLinkText(linkText); + } + + @Override + public String toString() { + return "By.partialLinkText: " + linkText; + } + }; + } + + /** + * Locates an element by matching its HTML name attribute. + * + * @param name The value of the HTML name attribute. + * @return A By instance that locates elements by the HTML name attribute. + */ + public static By name(final String name) { + throwIfNull(name); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByName(name); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByName(name); + } + + @Override + public String toString() { + return "By.name: " + name; + } + }; + } + + /** + * Locates an element by matching its class name. + * @param className The class name + * @return A By instance that locates elements by their class name attribute. + */ + public static By className(final String className) { + throwIfNull(className); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByClassName(className); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByClassName(className); + } + + @Override + public String toString() { + return "By.className: " + className; + } + }; + } + + /** + * Locates an element by matching its css property. + * + * @param css The css property. + * @return A By instance that locates elements by their css property. + */ + public static By css(final String css) { + throwIfNull(css); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByCss(css); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByCss(css); + } + + @Override + public String toString() { + return "By.css: " + css; + } + }; + } + + /** + * Locates an element by matching its HTML tag name. + * + * @param tagName The HTML tag name to look for. + * @return A By instance that locates elements using the name of the + * HTML tag. + */ + public static By tagName(final String tagName) { + throwIfNull(tagName); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByTagName(tagName); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByTagName(tagName); + } + + @Override + public String toString() { + return "By.tagName: " + tagName; + } + }; + } + + /** + * Locates an element using an XPath expression. + * + * <p>When using XPath, be aware that this follows standard conventions: a + * search prefixed with "//" will search the entire document, not just the + * children of the current node. Use ".//" to limit your search to the + * children of this {@link android.webkit.webdriver.WebElement}. + * + * @param xpath The XPath expression to use. + * @return A By instance that locates elements using the given XPath. + */ + public static By xpath(final String xpath) { + throwIfNull(xpath); + return new By() { + @Override + public WebElement findElement(WebElement element) { + return element.findElementByXPath(xpath); + } + + @Override + public List<WebElement> findElements(WebElement element) { + return element.findElementsByXPath(xpath); + } + + @Override + public String toString() { + return "By.xpath: " + xpath; + } + }; + } + + private static void throwIfNull(String argument) { + if (argument == null) { + throw new IllegalArgumentException( + "Cannot find elements with null locator."); + } + } +} diff --git a/core/java/android/webkit/webdriver/WebDriver.java b/core/java/android/webkit/webdriver/WebDriver.java new file mode 100644 index 0000000..79e6523 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebDriver.java @@ -0,0 +1,843 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +import android.graphics.Point; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.view.InputDevice; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.webkit.WebView; +import android.webkit.WebViewCore; + +import com.google.android.collect.Lists; +import com.google.android.collect.Maps; + +import com.android.internal.R; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Drives a web application by controlling the WebView. This class + * provides a DOM-like API allowing to get information about the page, + * navigate, and interact with the web application. This is particularly useful + * for testing a web application. + * + * <p/>{@link android.webkit.webdriver.WebDriver} should be created in the main + * thread, and invoked from another thread. Here is a sample usage: + * + * public class WebDriverStubActivity extends Activity { + * private WebDriver mDriver; + * + * public void onCreate(Bundle savedInstanceState) { + * super.onCreate(savedInstanceState); + * WebView view = new WebView(this); + * mDriver = new WebDriver(view); + * setContentView(view); + * } + * + * + * public WebDriver getDriver() { + * return mDriver; + * } + *} + * + * public class WebDriverTest extends + * ActivityInstrumentationTestCase2<WebDriverStubActivity>{ + * private WebDriver mDriver; + * + * public WebDriverTest() { + * super(WebDriverStubActivity.class); + * } + * + * protected void setUp() throws Exception { + * super.setUp(); + * mDriver = getActivity().getDriver(); + * } + * + * public void testGoogle() { + * mDriver.get("http://google.com"); + * WebElement searchBox = mDriver.findElement(By.name("q")); + * q.sendKeys("Cheese!"); + * q.submit(); + * assertTrue(mDriver.findElements(By.partialLinkText("Cheese")).size() > 0); + * } + *} + * + * @hide + */ +public class WebDriver { + // Timeout for page load in milliseconds. + private static final int LOADING_TIMEOUT = 30000; + // Timeout for executing JavaScript in the WebView in milliseconds. + private static final int JS_EXECUTION_TIMEOUT = 10000; + // Timeout for the MotionEvent to be completely handled + private static final int MOTION_EVENT_TIMEOUT = 1000; + // Timeout for detecting a new page load + private static final int PAGE_STARTED_LOADING = 500; + // Timeout for handling KeyEvents + private static final int KEY_EVENT_TIMEOUT = 2000; + + // Commands posted to the handler + private static final int CMD_GET_URL = 1; + private static final int CMD_EXECUTE_SCRIPT = 2; + private static final int CMD_SEND_TOUCH = 3; + private static final int CMD_SEND_KEYS = 4; + private static final int CMD_NAV_REFRESH = 5; + private static final int CMD_NAV_BACK = 6; + private static final int CMD_NAV_FORWARD = 7; + private static final int CMD_SEND_KEYCODE = 8; + private static final int CMD_MOVE_CURSOR_RIGHTMOST_POS = 9; + private static final int CMD_MESSAGE_RELAY_ECHO = 10; + + private static final String ELEMENT_KEY = "ELEMENT"; + private static final String STATUS = "status"; + private static final String VALUE = "value"; + + private static final long MAIN_THREAD = Thread.currentThread().getId(); + + // This is updated by a callabck from JavaScript when the result is ready. + private String mJsResult; + + // Used for synchronization + private final Object mSyncObject; + private final Object mSyncPageLoad; + + // Updated when the command is done executing in the main thread. + private volatile boolean mCommandDone; + // Used by WebViewClientWrapper.onPageStarted() to notify that + // a page started loading. + private volatile boolean mPageStartedLoading; + // Used by WebChromeClientWrapper.onProgressChanged to notify when + // a page finished loading. + private volatile boolean mPageFinishedLoading; + private WebView mWebView; + private Navigation mNavigation; + // This WebElement represents the object document.documentElement + private WebElement mDocumentElement; + + + // This Handler runs in the main UI thread. + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case CMD_GET_URL: + final String url = (String) msg.obj; + mWebView.loadUrl(url); + break; + case CMD_EXECUTE_SCRIPT: + mWebView.loadUrl("javascript:" + (String) msg.obj); + break; + case CMD_MESSAGE_RELAY_ECHO: + notifyCommandDone(); + break; + case CMD_SEND_TOUCH: + touchScreen((Point) msg.obj); + notifyCommandDone(); + break; + case CMD_SEND_KEYS: + dispatchKeys((CharSequence[]) msg.obj); + notifyCommandDone(); + break; + case CMD_NAV_REFRESH: + mWebView.reload(); + break; + case CMD_NAV_BACK: + mWebView.goBack(); + break; + case CMD_NAV_FORWARD: + mWebView.goForward(); + break; + case CMD_SEND_KEYCODE: + dispatchKeyCodes((int[]) msg.obj); + notifyCommandDone(); + break; + case CMD_MOVE_CURSOR_RIGHTMOST_POS: + moveCursorToLeftMostPos((String) msg.obj); + notifyCommandDone(); + break; + } + } + }; + + /** + * Error codes from the WebDriver wire protocol + * http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes + */ + private enum ErrorCode { + SUCCESS(0), + NO_SUCH_ELEMENT(7), + NO_SUCH_FRAME(8), + UNKNOWN_COMMAND(9), + UNSUPPORTED_OPERATION(9), // Alias + STALE_ELEMENT_REFERENCE(10), + ELEMENT_NOT_VISISBLE(11), + INVALID_ELEMENT_STATE(12), + UNKNOWN_ERROR(13), + ELEMENT_NOT_SELECTABLE(15), + XPATH_LOOKUP_ERROR(19), + NO_SUCH_WINDOW(23), + INVALID_COOKIE_DOMAIN(24), + UNABLE_TO_SET_COOKIE(25), + MODAL_DIALOG_OPENED(26), + MODAL_DIALOG_OPEN(27), + SCRIPT_TIMEOUT(28); + + private final int mCode; + private static ErrorCode[] values = ErrorCode.values(); + + ErrorCode(int code) { + this.mCode = code; + } + + public int getCode() { + return mCode; + } + + public static ErrorCode get(final int intValue) { + for (int i = 0; i < values.length; i++) { + if (values[i].getCode() == intValue) { + return values[i]; + } + } + return UNKNOWN_ERROR; + } + } + + public WebDriver(WebView webview) { + this.mWebView = webview; + mWebView.requestFocus(); + if (mWebView == null) { + throw new IllegalArgumentException("WebView cannot be null"); + } + if (!mWebView.getSettings().getJavaScriptEnabled()) { + throw new RuntimeException("Javascript is disabled in the WebView. " + + "Enable it to use WebDriver"); + } + shouldRunInMainThread(true); + + mSyncObject = new Object(); + mSyncPageLoad = new Object(); + this.mWebView = webview; + WebChromeClientWrapper chromeWrapper = new WebChromeClientWrapper( + webview.getWebChromeClient(), this); + mWebView.setWebChromeClient(chromeWrapper); + WebViewClientWrapper viewWrapper = new WebViewClientWrapper( + webview.getWebViewClient(), this); + mWebView.setWebViewClient(viewWrapper); + mWebView.addJavascriptInterface(new JavascriptResultReady(), + "webdriver"); + mDocumentElement = new WebElement(this, ""); + mNavigation = new Navigation(); + } + + /** + * @return The title of the current page, null if not set. + */ + public String getTitle() { + return mWebView.getTitle(); + } + + /** + * Loads a URL in the WebView. This function is blocking and will return + * when the page has finished loading. + * + * @param url The URL to load. + */ + public void get(String url) { + mNavigation.to(url); + } + + /** + * @return The source page of the currently loaded page in WebView. + */ + public String getPageSource() { + return (String) executeScript("return new XMLSerializer()." + + "serializeToString(document);"); + } + + /** + * Find the first {@link android.webkit.webdriver.WebElement} using the + * given method. + * + * @param by The locating mechanism to use. + * @return The first matching element on the current context. + * @throws {@link android.webkit.webdriver.WebElementNotFoundException} if + * no matching element was found. + */ + public WebElement findElement(By by) { + checkNotNull(mDocumentElement, "Load a page using WebDriver.get() " + + "before looking for elements."); + return by.findElement(mDocumentElement); + } + + /** + * Finds all {@link android.webkit.webdriver.WebElement} within the page + * using the given method. + * + * @param by The locating mechanism to use. + * @return A list of all {@link android.webkit.webdriver.WebElement} found, + * or an empty list if nothing matches. + */ + public List<WebElement> findElements(By by) { + checkNotNull(mDocumentElement, "Load a page using WebDriver.get() " + + "before looking for elements."); + return by.findElements(mDocumentElement); + } + + /** + * Clears the WebView's state and closes associated views. + */ + public void quit() { + mWebView.clearCache(true); + mWebView.clearFormData(); + mWebView.clearHistory(); + mWebView.clearSslPreferences(); + mWebView.clearView(); + mWebView.removeAllViewsInLayout(); + } + + /** + * Executes javascript in the context of the main frame. + * + * If the script has a return value the following happens: + * <ul> + * <li>For an HTML element, this method returns a WebElement</li> + * <li>For a decimal, a Double is returned</li> + * <li>For non-decimal number, a Long is returned</li> + * <li>For a boolean, a Boolean is returned</li> + * <li>For all other cases, a String is returned</li> + * <li>For an array, this returns a List<Object> with each object + * following the rules above.</li> + * <li>For an object literal this returns a Map<String, Object>. Note that + * Object literals keys can only be Strings. Non Strings keys will + * be filtered out.</li> + * </ul> + * + * <p> Arguments must be a number, a boolean, a string a WebElement or + * a list of any combination of the above. The arguments will be made + * available to the javascript via the "arguments" magic variable, + * as if the function was called via "Function.apply". + * + * @param script The JavaScript to execute. + * @param args The arguments to the script. Can be any of a number, boolean, + * string, WebElement or a List of those. + * @return A Boolean, Long, Double, String, WebElement, List or null. + */ + public Object executeScript(final String script, final Object... args) { + String scriptArgs = "[" + convertToJsArgs(args) + "]"; + String injectScriptJs = getResourceAsString(R.raw.execute_script_android); + return executeRawJavascript("(" + injectScriptJs + + ")(" + escapeAndQuote(script) + ", " + scriptArgs + ", true)"); + } + + public Navigation navigate() { + return mNavigation; + } + + + /** + * @hide + */ + public class Navigation { + /* package */ Navigation () {} + + public void back() { + navigate(CMD_NAV_BACK, null); + } + + public void forward() { + navigate(CMD_NAV_FORWARD, null); + } + + public void to(String url) { + navigate(CMD_GET_URL, url); + } + + public void refresh() { + navigate(CMD_NAV_REFRESH, null); + } + + private void navigate(int command, String url) { + synchronized (mSyncPageLoad) { + mPageFinishedLoading = false; + Message msg = mHandler.obtainMessage(command); + msg.obj = url; + mHandler.sendMessage(msg); + waitForPageLoad(); + } + } + } + + /** + * Converts the arguments passed to a JavaScript friendly format. + * + * @param args The arguments to convert. + * @return Comma separated Strings containing the arguments. + */ + /* package */ String convertToJsArgs(final Object... args) { + StringBuilder toReturn = new StringBuilder(); + int length = args.length; + for (int i = 0; i < length; i++) { + toReturn.append((i > 0) ? "," : ""); + if (args[i] instanceof List<?>) { + toReturn.append("["); + List<Object> aList = (List<Object>) args[i]; + for (int j = 0 ; j < aList.size(); j++) { + String comma = ((j == 0) ? "" : ","); + toReturn.append(comma + convertToJsArgs(aList.get(j))); + } + toReturn.append("]"); + } else if (args[i] instanceof Map<?, ?>) { + Map<Object, Object> aMap = (Map<Object, Object>) args[i]; + String toAdd = "{"; + for (Object key: aMap.keySet()) { + toAdd += key + ":" + + convertToJsArgs(aMap.get(key)) + ","; + } + toReturn.append(toAdd.substring(0, toAdd.length() -1) + "}"); + } else if (args[i] instanceof WebElement) { + // WebElement are represented in JavaScript by Objects as + // follow: {ELEMENT:"id"} where "id" refers to the id + // of the HTML element in the javascript cache that can + // be accessed throught bot.inject.cache.getCache_() + toReturn.append("{\"" + ELEMENT_KEY + "\":\"" + + ((WebElement) args[i]).getId() + "\"}"); + } else if (args[i] instanceof Number || args[i] instanceof Boolean) { + toReturn.append(String.valueOf(args[i])); + } else if (args[i] instanceof String) { + toReturn.append(escapeAndQuote((String) args[i])); + } else { + throw new IllegalArgumentException( + "Javascript arguments can be " + + "a Number, a Boolean, a String, a WebElement, " + + "or a List or a Map of those. Got: " + + ((args[i] == null) ? "null" : args[i].getClass() + + ", value: " + args[i].toString())); + } + } + return toReturn.toString(); + } + + /* package */ Object executeRawJavascript(final String script) { + if (mWebView.getUrl() == null) { + throw new WebDriverException("Cannot operate on a blank page. " + + "Load a page using WebDriver.get()."); + } + String result = executeCommand(CMD_EXECUTE_SCRIPT, + "if (!window.webdriver || !window.webdriver.resultReady) {" + + " return;" + + "}" + + "window.webdriver.resultReady(" + script + ")", + JS_EXECUTION_TIMEOUT); + if (result == null || "undefined".equals(result)) { + return null; + } + try { + JSONObject json = new JSONObject(result); + throwIfError(json); + Object value = json.get(VALUE); + return convertJsonToJavaObject(value); + } catch (JSONException e) { + throw new RuntimeException("Failed to parse JavaScript result: " + + result.toString(), e); + } + } + + /* package */ String getResourceAsString(final int resourceId) { + InputStream is = mWebView.getResources().openRawResource(resourceId); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line = null; + try { + while ((line = br.readLine()) != null) { + sb.append(line); + } + br.close(); + is.close(); + } catch (IOException e) { + throw new RuntimeException("Failed to open JavaScript resource.", e); + } + return sb.toString(); + } + + /* package */ void sendTouchScreen(Point coords) { + // Reset state + resetPageLoadState(); + executeCommand(CMD_SEND_TOUCH, coords,LOADING_TIMEOUT); + // Wait for the events to be fully handled + waitForMessageRelay(MOTION_EVENT_TIMEOUT); + + // If a page started loading, block until page finishes loading + waitForPageLoadIfNeeded(); + } + + /* package */ void resetPageLoadState() { + synchronized (mSyncPageLoad) { + mPageStartedLoading = false; + mPageFinishedLoading = false; + } + } + + /* package */ void waitForPageLoadIfNeeded() { + synchronized (mSyncPageLoad) { + Long end = System.currentTimeMillis() + PAGE_STARTED_LOADING; + // Wait PAGE_STARTED_LOADING milliseconds to see if we detect a + // page load. + while (!mPageStartedLoading && (System.currentTimeMillis() <= end)) { + try { + // This is notified by WebChromeClientWrapper#onProgressChanged + // when the page finished loading. + mSyncPageLoad.wait(PAGE_STARTED_LOADING); + } catch (InterruptedException e) { + new RuntimeException(e); + } + } + if (mPageStartedLoading) { + waitForPageLoad(); + } + } + } + + private void touchScreen(Point coords) { + // Convert to screen coords + // screen = JS x zoom - offset + float zoom = mWebView.getScale(); + float xOffset = mWebView.getX(); + float yOffset = mWebView.getY(); + Point screenCoords = new Point( (int)(coords.x*zoom - xOffset), + (int)(coords.y*zoom - yOffset)); + + long downTime = SystemClock.uptimeMillis(); + MotionEvent down = MotionEvent.obtain(downTime, + SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, screenCoords.x, + screenCoords.y, 0); + down.setSource(InputDevice.SOURCE_TOUCHSCREEN); + MotionEvent up = MotionEvent.obtain(downTime, + SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, screenCoords.x, + screenCoords.y, 0); + up.setSource(InputDevice.SOURCE_TOUCHSCREEN); + // Dispatch the events to WebView + mWebView.dispatchTouchEvent(down); + mWebView.dispatchTouchEvent(up); + } + + /* package */ void notifyPageStartedLoading() { + synchronized (mSyncPageLoad) { + mPageStartedLoading = true; + mSyncPageLoad.notify(); + } + } + + /* package */ void notifyPageFinishedLoading() { + synchronized (mSyncPageLoad) { + mPageFinishedLoading = true; + mSyncPageLoad.notify(); + } + } + + /** + * + * @param keys The first element of the CharSequence should be the + * existing value in the text input, or the empty string if none. + */ + /* package */ void sendKeys(CharSequence[] keys) { + executeCommand(CMD_SEND_KEYS, keys, KEY_EVENT_TIMEOUT); + // Wait for all KeyEvents to be handled + waitForMessageRelay(KEY_EVENT_TIMEOUT); + } + + /* package */ void sendKeyCodes(int[] keycodes) { + executeCommand(CMD_SEND_KEYCODE, keycodes, KEY_EVENT_TIMEOUT); + // Wait for all KeyEvents to be handled + waitForMessageRelay(KEY_EVENT_TIMEOUT); + } + + /* package */ void moveCursorToRightMostPosition(String value) { + executeCommand(CMD_MOVE_CURSOR_RIGHTMOST_POS, value, KEY_EVENT_TIMEOUT); + waitForMessageRelay(KEY_EVENT_TIMEOUT); + } + + private void moveCursorToLeftMostPos(String value) { + // If there is text, move the cursor to the rightmost position + if (value != null && !value.equals("")) { + long downTime = SystemClock.uptimeMillis(); + KeyEvent down = new KeyEvent(downTime, SystemClock.uptimeMillis(), + KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT, 0); + KeyEvent up = new KeyEvent(downTime, SystemClock.uptimeMillis(), + KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DPAD_RIGHT, + value.length()); + mWebView.dispatchKeyEvent(down); + mWebView.dispatchKeyEvent(up); + } + } + + private void dispatchKeyCodes(int[] keycodes) { + for (int i = 0; i < keycodes.length; i++) { + KeyEvent down = new KeyEvent(KeyEvent.ACTION_DOWN, keycodes[i]); + KeyEvent up = new KeyEvent(KeyEvent.ACTION_UP, keycodes[i]); + mWebView.dispatchKeyEvent(down); + mWebView.dispatchKeyEvent(up); + } + } + + private void dispatchKeys(CharSequence[] keys) { + KeyCharacterMap chararcterMap = KeyCharacterMap.load( + KeyCharacterMap.VIRTUAL_KEYBOARD); + for (int i = 0; i < keys.length; i++) { + CharSequence s = keys[i]; + for (int j = 0; j < s.length(); j++) { + KeyEvent[] events = + chararcterMap.getEvents(new char[]{s.charAt(j)}); + for (KeyEvent e : events) { + mWebView.dispatchKeyEvent(e); + } + } + } + } + + private void waitForMessageRelay(long timeout) { + synchronized (mSyncObject) { + mCommandDone = false; + } + Message msg = Message.obtain(); + msg.what = WebViewCore.EventHub.MESSAGE_RELAY; + Message echo = mHandler.obtainMessage(CMD_MESSAGE_RELAY_ECHO); + msg.obj = echo; + + mWebView.getWebViewCore().sendMessage(msg); + synchronized (mSyncObject) { + long end = System.currentTimeMillis() + timeout; + while (!mCommandDone && (System.currentTimeMillis() <= end)) { + try { + // This is notifed by the mHandler when it receives the + // MESSAGE_RELAY back + mSyncObject.wait(timeout); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + } + + private void waitForPageLoad() { + long endLoad = System.currentTimeMillis() + LOADING_TIMEOUT; + while (!mPageFinishedLoading + && (System.currentTimeMillis() <= endLoad)) { + try { + mSyncPageLoad.wait(LOADING_TIMEOUT); + } catch (InterruptedException e) { + throw new RuntimeException(); + } + } + } + + /** + * Wraps the given string into quotes and escape existing quotes + * and backslashes. + * "foo" -> "\"foo\"" + * "foo\"" -> "\"foo\\\"\"" + * "fo\o" -> "\"fo\\o\"" + * + * @param toWrap The String to wrap in quotes + * @return a String wrapping the original String in quotes + */ + private static String escapeAndQuote(final String toWrap) { + StringBuilder toReturn = new StringBuilder("\""); + for (int i = 0; i < toWrap.length(); i++) { + char c = toWrap.charAt(i); + if (c == '\"') { + toReturn.append("\\\""); + } else if (c == '\\') { + toReturn.append("\\\\"); + } else { + toReturn.append(c); + } + } + toReturn.append("\""); + return toReturn.toString(); + } + + private Object convertJsonToJavaObject(final Object toConvert) { + try { + if (toConvert == null + || toConvert.equals(null) + || "undefined".equals(toConvert) + || "null".equals(toConvert)) { + return null; + } else if (toConvert instanceof Boolean) { + return toConvert; + } else if (toConvert instanceof Double + || toConvert instanceof Float) { + return Double.valueOf(String.valueOf(toConvert)); + } else if (toConvert instanceof Integer + || toConvert instanceof Long) { + return Long.valueOf(String.valueOf(toConvert)); + } else if (toConvert instanceof JSONArray) { // List + return convertJsonArrayToList((JSONArray) toConvert); + } else if (toConvert instanceof JSONObject) { // Map or WebElment + JSONObject map = (JSONObject) toConvert; + if (map.opt(ELEMENT_KEY) != null) { // WebElement + return new WebElement(this, (String) map.get(ELEMENT_KEY)); + } else { // Map + return convertJsonObjectToMap(map); + } + } else { + return toConvert.toString(); + } + } catch (JSONException e) { + throw new RuntimeException("Failed to parse JavaScript result: " + + toConvert.toString(), e); + } + } + + private List<Object> convertJsonArrayToList(final JSONArray json) { + List<Object> toReturn = Lists.newArrayList(); + for (int i = 0; i < json.length(); i++) { + try { + toReturn.add(convertJsonToJavaObject(json.get(i))); + } catch (JSONException e) { + throw new RuntimeException("Failed to parse JSON: " + + json.toString(), e); + } + } + return toReturn; + } + + private Map<Object, Object> convertJsonObjectToMap(final JSONObject json) { + Map<Object, Object> toReturn = Maps.newHashMap(); + for (Iterator it = json.keys(); it.hasNext();) { + String key = (String) it.next(); + try { + Object value = json.get(key); + toReturn.put(convertJsonToJavaObject(key), + convertJsonToJavaObject(value)); + } catch (JSONException e) { + throw new RuntimeException("Failed to parse JSON:" + + json.toString(), e); + } + } + return toReturn; + } + + private void throwIfError(final JSONObject jsonObject) { + ErrorCode status; + String errorMsg; + try { + status = ErrorCode.get((Integer) jsonObject.get(STATUS)); + errorMsg = String.valueOf(jsonObject.get(VALUE)); + } catch (JSONException e) { + throw new RuntimeException("Failed to parse JSON Object: " + + jsonObject, e); + } + switch (status) { + case SUCCESS: + return; + case NO_SUCH_ELEMENT: + throw new WebElementNotFoundException("Could not find " + + "WebElement."); + case STALE_ELEMENT_REFERENCE: + throw new WebElementStaleException("WebElement is stale."); + default: + throw new WebDriverException("Error: " + errorMsg); + } + } + + private void shouldRunInMainThread(boolean value) { + assert (value == (MAIN_THREAD == Thread.currentThread().getId())); + } + + /** + * Interface called from JavaScript when the result is ready. + */ + private class JavascriptResultReady { + + /** + * A callback from JavaScript to Java that passes the result as a + * parameter. This method is available from the WebView's + * JavaScript DOM as window.webdriver.resultReady(). + * + * @param result The result that should be sent to Java from Javascript. + */ + public void resultReady(final String result) { + synchronized (mSyncObject) { + mJsResult = result; + mCommandDone = true; + mSyncObject.notify(); + } + } + } + + /* package */ void notifyCommandDone() { + synchronized (mSyncObject) { + mCommandDone = true; + mSyncObject.notify(); + } + } + + /** + * Executes the given command by posting a message to mHandler. This thread + * will block until the command which runs in the main thread is done. + * + * @param command The command to run. + * @param arg The argument for that command. + * @param timeout A timeout in milliseconds. + */ + private String executeCommand(int command, final Object arg, long timeout) { + shouldRunInMainThread(false); + synchronized (mSyncObject) { + mCommandDone = false; + Message msg = mHandler.obtainMessage(command); + msg.obj = arg; + mHandler.sendMessage(msg); + + long end = System.currentTimeMillis() + timeout; + while (!mCommandDone) { + if (System.currentTimeMillis() >= end) { + throw new RuntimeException("Timeout executing command: " + + command); + } + try { + mSyncObject.wait(timeout); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + return mJsResult; + } + + private void checkNotNull(Object obj, String errosMsg) { + if (obj == null) { + throw new NullPointerException(errosMsg); + } + } +} diff --git a/core/java/android/webkit/webdriver/WebDriverException.java b/core/java/android/webkit/webdriver/WebDriverException.java new file mode 100644 index 0000000..1a579c2 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebDriverException.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +/** + * @hide + */ +public class WebDriverException extends RuntimeException { + public WebDriverException() { + super(); + } + + public WebDriverException(String reason) { + super(reason); + } + + public WebDriverException(String reason, Throwable cause) { + super(reason, cause); + } + + public WebDriverException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/webkit/webdriver/WebElement.java b/core/java/android/webkit/webdriver/WebElement.java new file mode 100644 index 0000000..02c1595 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebElement.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +import android.graphics.Point; +import android.view.KeyEvent; + +import com.android.internal.R; + +import java.util.List; +import java.util.Map; + +/** + * Represents an HTML element. Typically most interactions with a web page + * will be performed through this class. + * + * @hide + */ +public class WebElement { + private final String mId; + private final WebDriver mDriver; + + private static final String LOCATOR_ID = "id"; + private static final String LOCATOR_LINK_TEXT = "linkText"; + private static final String LOCATOR_PARTIAL_LINK_TEXT = "partialLinkText"; + private static final String LOCATOR_NAME = "name"; + private static final String LOCATOR_CLASS_NAME = "className"; + private static final String LOCATOR_CSS = "css"; + private static final String LOCATOR_TAG_NAME = "tagName"; + private static final String LOCATOR_XPATH = "xpath"; + + /** + * Package constructor to prevent clients from creating a new WebElement + * instance. + * + * <p> A WebElement represents an HTML element on the page. + * The corresponding HTML element is stored in a JS cache in the page + * that can be accessed through JavaScript using "bot.inject.cache". + * + * @param driver The WebDriver instance to use. + * @param id The index of the HTML element in the JavaSctipt cache. + * document.documentElement object. + */ + /* package */ WebElement(final WebDriver driver, final String id) { + this.mId = id; + this.mDriver = driver; + } + + /** + * Finds the first {@link android.webkit.webdriver.WebElement} using the + * given method. + * + * @param by The locating mechanism to use. + * @return The first matching element on the current context. + */ + public WebElement findElement(final By by) { + return by.findElement(this); + } + + /** + * Finds all {@link android.webkit.webdriver.WebElement} within the page + * using the given method. + * + * @param by The locating mechanism to use. + * @return A list of all {@link android.webkit.webdriver.WebElement} found, + * or an empty list if nothing matches. + */ + public List<WebElement> findElements(final By by) { + return by.findElements(this); + } + + /** + * Gets the visisble (i.e. not hidden by CSS) innerText of this element, + * inlcuding sub-elements. + * + * @return the innerText of this element. + * @throws {@link android.webkit.webdriver.WebElementStaleException} if this + * element is stale, i.e. not on the current DOM. + */ + public String getText() { + String getText = mDriver.getResourceAsString(R.raw.get_text_android); + return (String) executeAtom(getText, this); + } + + /** + * Gets the value of an HTML attribute for this element or the value of the + * property with the same name if the attribute is not present. If neither + * is set, null is returned. + * + * @param attribute the HTML attribute. + * @return the value of that attribute or the value of the property with the + * same name if the attribute is not set, or null if neither are set. For + * boolean attribute values this will return the string "true" or "false". + */ + public String getAttribute(String attribute) { + String getAttribute = mDriver.getResourceAsString( + R.raw.get_attribute_value_android); + return (String) executeAtom(getAttribute, this, attribute); + } + + /** + * @return the tag name of this element. + */ + public String getTagName() { + return (String) mDriver.executeScript("return arguments[0].tagName;", + this); + } + + /** + * @return true if this element is enabled, false otherwise. + */ + public boolean isEnabled() { + String isEnabled = mDriver.getResourceAsString( + R.raw.is_enabled_android); + return (Boolean) executeAtom(isEnabled, this); + } + + /** + * Determines whether this element is selected or not. This applies to input + * elements such as checkboxes, options in a select, and radio buttons. + * + * @return True if this element is selected, false otherwise. + */ + public boolean isSelected() { + String isSelected = mDriver.getResourceAsString( + R.raw.is_selected_android); + return (Boolean) executeAtom(isSelected, this); + } + + /** + * Selects an element on the page. This works for selecting checkboxes, + * options in a select, and radio buttons. + */ + public void setSelected() { + String setSelected = mDriver.getResourceAsString( + R.raw.set_selected_android); + executeAtom(setSelected, this); + } + + /** + * This toggles the checkboxe state from selected to not selected, or + * from not selected to selected. + * + * @return True if the toggled element is selected, false otherwise. + */ + public boolean toggle() { + String toggle = mDriver.getResourceAsString(R.raw.toggle_android); + return (Boolean) executeAtom(toggle, this); + } + + /** + * Sends the KeyEvents for the given sequence of characters to the + * WebElement to simulate typing. The KeyEvents are generated using the + * device's {@link android.view.KeyCharacterMap.VIRTUAL_KEYBOARD}. + * + * @param keys The keys to send to this WebElement + */ + public void sendKeys(CharSequence... keys) { + if (keys == null || keys.length == 0) { + return; + } + click(); + mDriver.moveCursorToRightMostPosition(getAttribute("value")); + mDriver.sendKeys(keys); + } + + /** + * Use this to send one of the key code constants defined in + * {@link android.view.KeyEvent} + * + * @param keys + */ + public void sendKeyCodes(int... keys) { + if (keys == null || keys.length == 0) { + return; + } + click(); + mDriver.moveCursorToRightMostPosition(getAttribute("value")); + mDriver.sendKeyCodes(keys); + } + + /** + * Sends a touch event to the center coordinates of this WebElement. + */ + public void click() { + Point topLeft = getLocation(); + Point size = getSize(); + int jsX = topLeft.x + size.x/2; + int jsY = topLeft.y + size.y/2; + Point center = new Point(jsX, jsY); + mDriver.sendTouchScreen(center); + } + + /** + * Submits the form containing this WebElement. + */ + public void submit() { + mDriver.resetPageLoadState(); + String submit = mDriver.getResourceAsString(R.raw.submit_android); + executeAtom(submit, this); + mDriver.waitForPageLoadIfNeeded(); + } + + /** + * Clears the text value if this is a text entry element. Does nothing + * otherwise. + */ + public void clear() { + String value = getAttribute("value"); + if (value == null || value.equals("")) { + return; + } + int length = value.length(); + int[] keys = new int[length]; + for (int i = 0; i < length; i++) { + keys[i] = KeyEvent.KEYCODE_DEL; + } + sendKeyCodes(keys); + } + + /** + * @return the value of the given CSS property if found, null otherwise. + */ + public String getCssValue(String cssProperty) { + String getCssProp = mDriver.getResourceAsString( + R.raw.get_value_of_css_property_android); + return (String) executeAtom(getCssProp, this, cssProperty); + } + + /** + * Gets the width and height of the rendered element. + * + * @return a {@link android.graphics.Point}, where Point.x represents the + * width, and Point.y represents the height of the element. + */ + public Point getSize() { + String getSize = mDriver.getResourceAsString(R.raw.get_size_android); + Map<String, Long> map = (Map<String, Long>) executeAtom(getSize, this); + return new Point(map.get("width").intValue(), + map.get("height").intValue()); + } + + /** + * Gets the location of the top left corner of this element on the screen. + * If the element is not visisble, this will scroll to get the element into + * the visisble screen. + * + * @return a {@link android.graphics.Point} containing the x and y + * coordinates of the top left corner of this element. + */ + public Point getLocation() { + String getLocation = mDriver.getResourceAsString( + R.raw.get_top_left_coordinates_android); + Map<String,Long> map = (Map<String, Long>) executeAtom(getLocation, + this); + return new Point(map.get("x").intValue(), map.get("y").intValue()); + } + + /** + * @return True if the WebElement is displayed on the screen, + * false otherwise. + */ + public boolean isDisplayed() { + String isDisplayed = mDriver.getResourceAsString( + R.raw.is_displayed_android); + return (Boolean) executeAtom(isDisplayed, this); + } + + /*package*/ String getId() { + return mId; + } + + /* package */ WebElement findElementById(final String locator) { + return findElement(LOCATOR_ID, locator); + } + + /* package */ WebElement findElementByLinkText(final String linkText) { + return findElement(LOCATOR_LINK_TEXT, linkText); + } + + /* package */ WebElement findElementByPartialLinkText( + final String linkText) { + return findElement(LOCATOR_PARTIAL_LINK_TEXT, linkText); + } + + /* package */ WebElement findElementByName(final String name) { + return findElement(LOCATOR_NAME, name); + } + + /* package */ WebElement findElementByClassName(final String className) { + return findElement(LOCATOR_CLASS_NAME, className); + } + + /* package */ WebElement findElementByCss(final String css) { + return findElement(LOCATOR_CSS, css); + } + + /* package */ WebElement findElementByTagName(final String tagName) { + return findElement(LOCATOR_TAG_NAME, tagName); + } + + /* package */ WebElement findElementByXPath(final String xpath) { + return findElement(LOCATOR_XPATH, xpath); + } + + /* package */ List<WebElement> findElementsById(final String locator) { + return findElements(LOCATOR_ID, locator); + } + + /* package */ List<WebElement> findElementsByLinkText(final String linkText) { + return findElements(LOCATOR_LINK_TEXT, linkText); + } + + /* package */ List<WebElement> findElementsByPartialLinkText( + final String linkText) { + return findElements(LOCATOR_PARTIAL_LINK_TEXT, linkText); + } + + /* package */ List<WebElement> findElementsByName(final String name) { + return findElements(LOCATOR_NAME, name); + } + + /* package */ List<WebElement> findElementsByClassName(final String className) { + return findElements(LOCATOR_CLASS_NAME, className); + } + + /* package */ List<WebElement> findElementsByCss(final String css) { + return findElements(LOCATOR_CSS, css); + } + + /* package */ List<WebElement> findElementsByTagName(final String tagName) { + return findElements(LOCATOR_TAG_NAME, tagName); + } + + /* package */ List<WebElement> findElementsByXPath(final String xpath) { + return findElements(LOCATOR_XPATH, xpath); + } + + private Object executeAtom(final String atom, final Object... args) { + String scriptArgs = mDriver.convertToJsArgs(args); + return mDriver.executeRawJavascript("(" + + atom + ")(" + scriptArgs + ")"); + } + + private List<WebElement> findElements(String strategy, String locator) { + String findElements = mDriver.getResourceAsString( + R.raw.find_elements_android); + if (mId.equals("")) { + return (List<WebElement>) executeAtom(findElements, + strategy, locator); + } else { + return (List<WebElement>) executeAtom(findElements, + strategy, locator, this); + } + } + + private WebElement findElement(String strategy, String locator) { + String findElement = mDriver.getResourceAsString( + R.raw.find_element_android); + WebElement el; + if (mId.equals("")) { + el = (WebElement) executeAtom(findElement, + strategy, locator); + } else { + el = (WebElement) executeAtom(findElement, + strategy, locator, this); + } + if (el == null) { + throw new WebElementNotFoundException("Could not find element " + + "with " + strategy + ": " + locator); + } + return el; + } +} diff --git a/core/java/android/webkit/webdriver/WebElementNotFoundException.java b/core/java/android/webkit/webdriver/WebElementNotFoundException.java new file mode 100644 index 0000000..e66d279 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebElementNotFoundException.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +/** + * Thrown when a {@link android.webkit.webdriver.WebElement} is not found in the + * DOM of the page. + * @hide + */ +public class WebElementNotFoundException extends RuntimeException { + + public WebElementNotFoundException() { + super(); + } + + public WebElementNotFoundException(String reason) { + super(reason); + } + + public WebElementNotFoundException(String reason, Throwable cause) { + super(reason, cause); + } + + public WebElementNotFoundException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/webkit/webdriver/WebElementStaleException.java b/core/java/android/webkit/webdriver/WebElementStaleException.java new file mode 100644 index 0000000..c59e794 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebElementStaleException.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +/** + * Thrown when trying to access a {@link android.webkit.webdriver.WebElement} + * that is stale. This mean that the {@link android.webkit.webdriver.WebElement} + * is no longer present on the DOM of the page. + * @hide + */ +public class WebElementStaleException extends RuntimeException { + + public WebElementStaleException() { + super(); + } + + public WebElementStaleException(String reason) { + super(reason); + } + + public WebElementStaleException(String reason, Throwable cause) { + super(reason, cause); + } + + public WebElementStaleException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/webkit/webdriver/WebViewClient.java b/core/java/android/webkit/webdriver/WebViewClient.java new file mode 100644 index 0000000..c582b24 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebViewClient.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +import android.graphics.Bitmap; +import android.net.http.SslError; +import android.os.Message; +import android.view.KeyEvent; +import android.webkit.HttpAuthHandler; +import android.webkit.SslErrorHandler; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +/* package */ class WebViewClientWrapper extends WebViewClient { + private final WebViewClient mDelegate; + private final WebDriver mDriver; + + public WebViewClientWrapper(WebViewClient delegate, WebDriver driver) { + if (delegate == null) { + mDelegate = new WebViewClient(); + } else { + mDelegate = delegate; + } + this.mDriver = driver; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + return mDelegate.shouldOverrideUrlLoading(view, url); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + mDriver.notifyPageStartedLoading(); + mDelegate.onPageStarted(view, url, favicon); + } + + @Override + public void onPageFinished(WebView view, String url) { + mDelegate.onPageFinished(view, url); + } + + @Override + public void onLoadResource(WebView view, String url) { + mDelegate.onLoadResource(view, url); + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, + String url) { + return mDelegate.shouldInterceptRequest(view, url); + } + + @Override + public void onTooManyRedirects(WebView view, Message cancelMsg, + Message continueMsg) { + mDelegate.onTooManyRedirects(view, cancelMsg, continueMsg); + } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, + String failingUrl) { + mDelegate.onReceivedError(view, errorCode, description, failingUrl); + } + + @Override + public void onFormResubmission(WebView view, Message dontResend, + Message resend) { + mDelegate.onFormResubmission(view, dontResend, resend); + } + + @Override + public void doUpdateVisitedHistory(WebView view, String url, + boolean isReload) { + mDelegate.doUpdateVisitedHistory(view, url, isReload); + } + + @Override + public void onReceivedSslError(WebView view, SslErrorHandler handler, + SslError error) { + mDelegate.onReceivedSslError(view, handler, error); + } + + @Override + public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, + String host, String realm) { + mDelegate.onReceivedHttpAuthRequest(view, handler, host, realm); + } + + @Override + public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { + return mDelegate.shouldOverrideKeyEvent(view, event); + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + mDelegate.onUnhandledKeyEvent(view, event); + } + + @Override + public void onScaleChanged(WebView view, float oldScale, float newScale) { + mDelegate.onScaleChanged(view, oldScale, newScale); + } + + @Override + public void onReceivedLoginRequest(WebView view, String realm, + String account, String args) { + mDelegate.onReceivedLoginRequest(view, realm, account, args); + } +} diff --git a/core/java/android/webkit/webdriver/WebchromeClientWrapper.java b/core/java/android/webkit/webdriver/WebchromeClientWrapper.java new file mode 100644 index 0000000..a9e5d19 --- /dev/null +++ b/core/java/android/webkit/webdriver/WebchromeClientWrapper.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit.webdriver; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Message; +import android.view.View; +import android.webkit.ConsoleMessage; +import android.webkit.GeolocationPermissions; +import android.webkit.JsPromptResult; +import android.webkit.JsResult; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebStorage; +import android.webkit.WebView; + +/* package */ class WebChromeClientWrapper extends WebChromeClient { + + private final WebChromeClient mDelegate; + private final WebDriver mDriver; + + public WebChromeClientWrapper(WebChromeClient delegate, WebDriver driver) { + if (delegate == null) { + this.mDelegate = new WebChromeClient(); + } else { + this.mDelegate = delegate; + } + this.mDriver = driver; + } + + @Override + public void onProgressChanged(WebView view, int newProgress) { + if (newProgress == 100) { + mDriver.notifyPageFinishedLoading(); + } + mDelegate.onProgressChanged(view, newProgress); + } + + @Override + public void onReceivedTitle(WebView view, String title) { + mDelegate.onReceivedTitle(view, title); + } + + @Override + public void onReceivedIcon(WebView view, Bitmap icon) { + mDelegate.onReceivedIcon(view, icon); + } + + @Override + public void onReceivedTouchIconUrl(WebView view, String url, + boolean precomposed) { + mDelegate.onReceivedTouchIconUrl(view, url, precomposed); + } + + @Override + public void onShowCustomView(View view, + CustomViewCallback callback) { + mDelegate.onShowCustomView(view, callback); + } + + @Override + public void onHideCustomView() { + mDelegate.onHideCustomView(); + } + + @Override + public boolean onCreateWindow(WebView view, boolean dialog, + boolean userGesture, Message resultMsg) { + return mDelegate.onCreateWindow(view, dialog, userGesture, resultMsg); + } + + @Override + public void onRequestFocus(WebView view) { + mDelegate.onRequestFocus(view); + } + + @Override + public void onCloseWindow(WebView window) { + mDelegate.onCloseWindow(window); + } + + @Override + public boolean onJsAlert(WebView view, String url, String message, + JsResult result) { + return mDelegate.onJsAlert(view, url, message, result); + } + + @Override + public boolean onJsConfirm(WebView view, String url, String message, + JsResult result) { + return mDelegate.onJsConfirm(view, url, message, result); + } + + @Override + public boolean onJsPrompt(WebView view, String url, String message, + String defaultValue, JsPromptResult result) { + return mDelegate.onJsPrompt(view, url, message, defaultValue, result); + } + + @Override + public boolean onJsBeforeUnload(WebView view, String url, String message, + JsResult result) { + return mDelegate.onJsBeforeUnload(view, url, message, result); + } + + @Override + public void onExceededDatabaseQuota(String url, String databaseIdentifier, + long currentQuota, long estimatedSize, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + mDelegate.onExceededDatabaseQuota(url, databaseIdentifier, currentQuota, + estimatedSize, totalUsedQuota, quotaUpdater); + } + + @Override + public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + mDelegate.onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota, + quotaUpdater); + } + + @Override + public void onGeolocationPermissionsShowPrompt(String origin, + GeolocationPermissions.Callback callback) { + mDelegate.onGeolocationPermissionsShowPrompt(origin, callback); + } + + @Override + public void onGeolocationPermissionsHidePrompt() { + mDelegate.onGeolocationPermissionsHidePrompt(); + } + + @Override + public boolean onJsTimeout() { + return mDelegate.onJsTimeout(); + } + + @Override + public void onConsoleMessage(String message, int lineNumber, + String sourceID) { + mDelegate.onConsoleMessage(message, lineNumber, sourceID); + } + + @Override + public boolean onConsoleMessage(ConsoleMessage consoleMessage) { + return mDelegate.onConsoleMessage(consoleMessage); + } + + @Override + public Bitmap getDefaultVideoPoster() { + return mDelegate.getDefaultVideoPoster(); + } + + @Override + public View getVideoLoadingProgressView() { + return mDelegate.getVideoLoadingProgressView(); + } + + @Override + public void getVisitedHistory(ValueCallback<String[]> callback) { + mDelegate.getVisitedHistory(callback); + } + + @Override + public void openFileChooser(ValueCallback<Uri> uploadFile, + String acceptType) { + mDelegate.openFileChooser(uploadFile, acceptType); + } + + @Override + public void setInstallableWebApp() { + mDelegate.setInstallableWebApp(); + } + + @Override + public void setupAutoFill(Message msg) { + mDelegate.setupAutoFill(msg); + } +} diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 094f195..82dd5db 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -55,6 +55,7 @@ import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewTreeObserver; +import android.view.accessibility.AccessibilityEvent; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -2532,6 +2533,21 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return mContextMenuInfo; } + /** @hide */ + @Override + public boolean showContextMenu(float x, float y, int metaState) { + final int position = pointToPosition((int)x, (int)y); + if (position != INVALID_POSITION) { + final long id = mAdapter.getItemId(position); + View child = getChildAt(position - mFirstPosition); + if (child != null) { + mContextMenuInfo = createContextMenuInfo(child, position, id); + return super.showContextMenuForChild(AbsListView.this); + } + } + return super.showContextMenu(x, y, metaState); + } + @Override public boolean showContextMenuForChild(View originalView) { final int longPressPosition = getPositionForView(originalView); @@ -2806,7 +2822,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (ev.getEdgeFlags() != 0 && motionPosition < 0) { // If we couldn't find a view to click on, but the down event // was touching the edge, we will bail out and try again. - // This allows the edge correcting code in ViewRoot to try to + // This allows the edge correcting code in ViewAncestor to try to // find a nearby view to select return false; } @@ -2834,6 +2850,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te break; } } + + if (performButtonActionOnTouchDown(ev)) { + if (mTouchMode == TOUCH_MODE_DOWN) { + removeCallbacks(mPendingCheckForTap); + } + } break; } @@ -4529,8 +4551,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; } @@ -5009,7 +5032,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public boolean sendKeyEvent(KeyEvent event) { // Use our own input connection, since the filter // text view may not be shown in a window so has - // no ViewRoot to dispatch events with. + // no ViewAncestor to dispatch events with. return mDefInputConnection.sendKeyEvent(event); } }; diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index 0da73a4..2621e64 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -201,7 +201,8 @@ public abstract class AbsSeekBar extends ProgressBar { } @Override - void onProgressRefresh(float scale, boolean fromUser) { + void onProgressRefresh(float scale, boolean fromUser) { + super.onProgressRefresh(scale, fromUser); Drawable thumb = mThumb; if (thumb != null) { setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE); diff --git a/core/java/android/widget/AbsoluteLayout.java b/core/java/android/widget/AbsoluteLayout.java index ac82af7..7df6aab 100644 --- a/core/java/android/widget/AbsoluteLayout.java +++ b/core/java/android/widget/AbsoluteLayout.java @@ -141,6 +141,11 @@ public class AbsoluteLayout extends ViewGroup { return new LayoutParams(p); } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * Per-child layout information associated with AbsoluteLayout. * See diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index f16efbd..c4d05e9 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,43 @@ 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); + + return false; + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent 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); + selectedView.dispatchPopulateAccessibilityEvent(event); } + } - if (!populated) { - if (selectedView != null) { - event.setEnabled(selectedView.isEnabled()); - } - event.setItemCount(getCount()); - event.setCurrentItemIndex(getSelectedItemPosition()); - } + @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 void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); - return populated; + View selectedView = getSelectedView(); + if (selectedView != null) { + event.setEnabled(selectedView.isEnabled()); + } + event.setItemCount(getCount()); + event.setCurrentItemIndex(getSelectedItemPosition()); } @Override diff --git a/core/java/android/widget/AdapterViewAnimator.java b/core/java/android/widget/AdapterViewAnimator.java index 072992e..c773527 100644 --- a/core/java/android/widget/AdapterViewAnimator.java +++ b/core/java/android/widget/AdapterViewAnimator.java @@ -79,7 +79,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> /** * Map of the children of the {@link AdapterViewAnimator}. */ - HashMap<Integer, ViewAndIndex> mViewsMap = new HashMap<Integer, ViewAndIndex>(); + HashMap<Integer, ViewAndMetaData> mViewsMap = new HashMap<Integer, ViewAndMetaData>(); /** * List of views pending removal from the {@link AdapterViewAnimator} @@ -103,11 +103,6 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> int mCurrentWindowStartUnbounded = 0; /** - * Handler to post events to the main thread - */ - Handler mMainQueue; - - /** * Listens for data changes from the adapter */ AdapterDataSetObserver mDataSetObserver; @@ -163,15 +158,18 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> private static final int DEFAULT_ANIMATION_DURATION = 200; public AdapterViewAnimator(Context context) { - super(context); - initViewAnimator(); + this(context, null); } public AdapterViewAnimator(Context context, AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); + } + + public AdapterViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.AdapterViewAnimator); + com.android.internal.R.styleable.AdapterViewAnimator, defStyleAttr, 0); int resource = a.getResourceId( com.android.internal.R.styleable.AdapterViewAnimator_inAnimation, 0); if (resource > 0) { @@ -203,17 +201,21 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> * Initialize this {@link AdapterViewAnimator} */ private void initViewAnimator() { - mMainQueue = new Handler(Looper.myLooper()); mPreviousViews = new ArrayList<Integer>(); } - class ViewAndIndex { - ViewAndIndex(View v, int i) { - view = v; - index = i; - } + class ViewAndMetaData { View view; - int index; + int relativeIndex; + int adapterPosition; + long itemId; + + ViewAndMetaData(View view, int relativeIndex, int adapterPosition, long itemId) { + this.view = view; + this.relativeIndex = relativeIndex; + this.adapterPosition = adapterPosition; + this.itemId = itemId; + } } /** @@ -379,6 +381,15 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> } } + private ViewAndMetaData getMetaDataForChild(View child) { + for (ViewAndMetaData vm: mViewsMap.values()) { + if (vm.view == child) { + return vm; + } + } + return null; + } + LayoutParams createOrReuseLayoutParams(View v) { final ViewGroup.LayoutParams currentLp = v.getLayoutParams(); if (currentLp instanceof ViewGroup.LayoutParams) { @@ -481,7 +492,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> if (remove) { View previousView = mViewsMap.get(index).view; - int oldRelativeIndex = mViewsMap.get(index).index; + int oldRelativeIndex = mViewsMap.get(index).relativeIndex; mPreviousViews.add(index); transformViewForTransition(oldRelativeIndex, -1, previousView, animate); @@ -497,7 +508,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> int index = modulo(i, getWindowSize()); int oldRelativeIndex; if (mViewsMap.containsKey(index)) { - oldRelativeIndex = mViewsMap.get(index).index; + oldRelativeIndex = mViewsMap.get(index).relativeIndex; } else { oldRelativeIndex = -1; } @@ -510,14 +521,16 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> if (inOldRange) { View view = mViewsMap.get(index).view; - mViewsMap.get(index).index = newRelativeIndex; + mViewsMap.get(index).relativeIndex = newRelativeIndex; applyTransformForChildAtIndex(view, newRelativeIndex); transformViewForTransition(oldRelativeIndex, newRelativeIndex, view, animate); // Otherwise this view is new to the window } else { // Get the new view from the adapter, add it and apply any transform / animation - View newView = mAdapter.getView(modulo(i, adapterCount), null, this); + final int adapterPosition = modulo(i, adapterCount); + View newView = mAdapter.getView(adapterPosition, null, this); + long itemId = mAdapter.getItemId(adapterPosition); // We wrap the new view in a FrameLayout so as to respect the contract // with the adapter, that is, that we don't modify this view directly @@ -527,7 +540,8 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> if (newView != null) { fl.addView(newView); } - mViewsMap.put(index, new ViewAndIndex(fl, newRelativeIndex)); + mViewsMap.put(index, new ViewAndMetaData(fl, newRelativeIndex, + adapterPosition, itemId)); addChild(fl); applyTransformForChildAtIndex(fl, newRelativeIndex); transformViewForTransition(-1, newRelativeIndex, fl, animate); @@ -604,6 +618,7 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> case MotionEvent.ACTION_UP: { if (mTouchMode == TOUCH_MODE_DOWN_IN_CURRENT_VIEW) { final View v = getCurrentView(); + final ViewAndMetaData viewData = getMetaDataForChild(v); if (v != null) { if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) { final Handler handler = getHandler(); @@ -616,7 +631,12 @@ public abstract class AdapterViewAnimator extends AdapterView<Adapter> hideTapFeedback(v); post(new Runnable() { public void run() { - performItemClick(v, 0, 0); + if (viewData != null) { + performItemClick(v, viewData.adapterPosition, + viewData.itemId); + } else { + performItemClick(v, 0, 0); + } } }); } diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index bf63607..8d4aaea 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -199,11 +199,8 @@ public class CheckedTextView extends TextView implements Checkable { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = super.dispatchPopulateAccessibilityEvent(event); - if (!populated) { - event.setChecked(mChecked); - } - return populated; + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setChecked(mChecked); } } diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index 0df45cc..a730018 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -208,22 +208,9 @@ public abstract class CompoundButton extends Button implements Checkable { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = super.dispatchPopulateAccessibilityEvent(event); - - if (!populated) { - int resourceId = 0; - if (mChecked) { - resourceId = R.string.accessibility_compound_button_selected; - } else { - resourceId = R.string.accessibility_compound_button_unselected; - } - String state = getResources().getString(resourceId); - event.getText().add(state); - event.setChecked(mChecked); - } - - return populated; + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setChecked(mChecked); } @Override diff --git a/core/java/android/widget/CursorAdapter.java b/core/java/android/widget/CursorAdapter.java index 516162a..6c4c39d 100644 --- a/core/java/android/widget/CursorAdapter.java +++ b/core/java/android/widget/CursorAdapter.java @@ -21,7 +21,6 @@ import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; -import android.util.Config; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -440,7 +439,7 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable, */ protected void onContentChanged() { if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { - if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); + if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); mDataValid = mCursor.requery(); } } diff --git a/core/java/android/widget/CursorTreeAdapter.java b/core/java/android/widget/CursorTreeAdapter.java index 3fadf4c..44d1656 100644 --- a/core/java/android/widget/CursorTreeAdapter.java +++ b/core/java/android/widget/CursorTreeAdapter.java @@ -22,7 +22,6 @@ import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.os.Handler; -import android.util.Config; import android.util.Log; import android.util.SparseArray; import android.view.View; @@ -499,7 +498,7 @@ public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implem @Override public void onChange(boolean selfChange) { if (mAutoRequery && mCursor != null) { - if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + + if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); mDataValid = mCursor.requery(); } diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index 1d442db..30fb927 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -353,13 +353,14 @@ public class DatePicker extends FrameLayout { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + + final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_YEAR; String selectedDateUtterance = DateUtils.formatDateTime(mContext, mCurrentDate.getTimeInMillis(), flags); event.getText().add(selectedDateUtterance); - return true; } /** @@ -410,74 +411,28 @@ public class DatePicker extends FrameLayout { } /** - * Reorders the spinners according to the date format in the current - * {@link Locale}. + * Reorders the spinners according to the date format that is + * explicitly set by the user and if no such is set fall back + * to the current locale's default format. */ private void reorderSpinners() { - java.text.DateFormat format; - String order; - - /* - * If the user is in a locale where the medium date format is still - * numeric (Japanese and Czech, for example), respect the date format - * order setting. Otherwise, use the order that the locale says is - * appropriate for a spelled-out date. - */ - - if (getShortMonths()[0].startsWith("1")) { - format = DateFormat.getDateFormat(getContext()); - } else { - format = DateFormat.getMediumDateFormat(getContext()); - } - - if (format instanceof SimpleDateFormat) { - order = ((SimpleDateFormat) format).toPattern(); - } else { - // Shouldn't happen, but just in case. - order = new String(DateFormat.getDateFormatOrder(getContext())); - } - - /* - * Remove the 3 spinners from their parent and then add them back in the - * required order. - */ - LinearLayout parent = mSpinners; - parent.removeAllViews(); - - boolean quoted = false; - boolean didDay = false, didMonth = false, didYear = false; - - for (int i = 0; i < order.length(); i++) { - char c = order.charAt(i); - - if (c == '\'') { - quoted = !quoted; - } - - if (!quoted) { - if (c == DateFormat.DATE && !didDay) { - parent.addView(mDaySpinner); - didDay = true; - } else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) { - parent.addView(mMonthSpinner); - didMonth = true; - } else if (c == DateFormat.YEAR && !didYear) { - parent.addView(mYearSpinner); - didYear = true; - } + mSpinners.removeAllViews(); + char[] order = DateFormat.getDateFormatOrder(getContext()); + for (int i = 0; i < order.length; i++) { + switch (order[i]) { + case DateFormat.DATE: + mSpinners.addView(mDaySpinner); + break; + case DateFormat.MONTH: + mSpinners.addView(mMonthSpinner); + break; + case DateFormat.YEAR: + mSpinners.addView(mYearSpinner); + break; + default: + throw new IllegalArgumentException(); } } - - // Shouldn't happen, but just in case. - if (!didMonth) { - parent.addView(mMonthSpinner); - } - if (!didDay) { - parent.addView(mDaySpinner); - } - if (!didYear) { - parent.addView(mYearSpinner); - } } /** diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index f862368..ead9b4f 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -599,12 +599,35 @@ public class ExpandableListView extends ListView { * was already expanded, this will return false) */ public boolean expandGroup(int groupPos) { - boolean retValue = mConnector.expandGroup(groupPos); + return expandGroup(groupPos, false); + } + + /** + * Expand a group in the grouped list view + * + * @param groupPos the group to be expanded + * @param animate true if the expanding group should be animated in + * @return True if the group was expanded, false otherwise (if the group + * was already expanded, this will return false) + */ + public boolean expandGroup(int groupPos, boolean animate) { + PositionMetadata pm = mConnector.getFlattenedPos(ExpandableListPosition.obtain( + ExpandableListPosition.GROUP, groupPos, -1, -1)); + boolean retValue = mConnector.expandGroup(pm); if (mOnGroupExpandListener != null) { mOnGroupExpandListener.onGroupExpand(groupPos); } - + + if (animate) { + final int groupFlatPos = pm.position.flatListPos; + + final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount(); + smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos), + shiftedGroupPosition); + } + pm.recycle(); + return retValue; } diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index f659ead..6b498fe 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -16,6 +16,8 @@ package android.widget; +import java.util.ArrayList; + import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -23,14 +25,12 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.util.AttributeSet; +import android.view.Gravity; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; -import android.view.Gravity; import android.widget.RemoteViews.RemoteView; -import java.util.ArrayList; - /** * FrameLayout is designed to block out an area on the screen to display @@ -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 @@ -115,7 +115,7 @@ public class FrameLayout extends ViewGroup { } /** - * Describes how the foreground is positioned. Defaults to FILL. + * Describes how the foreground is positioned. Defaults to START and TOP. * * @param foregroundGravity See {@link android.view.Gravity} * @@ -124,8 +124,8 @@ public class FrameLayout extends ViewGroup { @android.view.RemotableViewMethod public void setForegroundGravity(int foregroundGravity) { if (mForegroundGravity != foregroundGravity) { - if ((foregroundGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { - foregroundGravity |= Gravity.LEFT; + if ((foregroundGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { + foregroundGravity |= Gravity.START; } if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { @@ -364,10 +364,10 @@ public class FrameLayout extends ViewGroup { gravity = DEFAULT_CHILD_GRAVITY; } - final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, isLayoutRtl()); final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; - switch (horizontalGravity) { + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: childLeft = parentLeft + lp.leftMargin; break; @@ -436,7 +436,7 @@ public class FrameLayout extends ViewGroup { } Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), - foreground.getIntrinsicHeight(), selfBounds, overlayBounds); + foreground.getIntrinsicHeight(), selfBounds, overlayBounds, isLayoutRtl()); foreground.setBounds(overlayBounds); } @@ -485,6 +485,11 @@ public class FrameLayout extends ViewGroup { return new FrameLayout.LayoutParams(getContext(), attrs); } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * {@inheritDoc} */ @@ -566,4 +571,3 @@ public class FrameLayout extends ViewGroup { } } } - diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java new file mode 100644 index 0000000..bda82a3 --- /dev/null +++ b/core/java/android/widget/GridLayout.java @@ -0,0 +1,2285 @@ +/* + * 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.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import com.android.internal.R.styleable; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static android.view.View.MeasureSpec.EXACTLY; +import static android.view.View.MeasureSpec.UNSPECIFIED; +import static java.lang.Math.max; +import static java.lang.Math.min; + +/** + * A layout that places its children in a rectangular <em>grid</em>. + * <p> + * The grid is composed of a set of infinitely thin lines that separate the + * viewing area into <em>cells</em>. Throughout the API, grid lines are referenced + * by grid <em>indices</em>. A grid with {@code N} columns + * has {@code N + 1} grid indices that run from {@code 0} + * through {@code N} inclusive. Regardless of how GridLayout is + * configured, grid index {@code 0} is fixed to the leading edge of the + * container and grid index {@code N} is fixed to its trailing edge + * (after padding is taken into account). + * + * <h4>Row and Column Groups</h4> + * + * Children occupy one or more contiguous cells, as defined + * by their {@link GridLayout.LayoutParams#rowGroup rowGroup} and + * {@link GridLayout.LayoutParams#columnGroup columnGroup} layout parameters. + * Each group specifies the set of rows or columns that are to be + * occupied; and how children should be aligned within the resulting group of cells. + * Although cells do not normally overlap in a GridLayout, GridLayout does + * not prevent children being defined to occupy the same cell or group of cells. + * In this case however, there is no guarantee that children will not themselves + * overlap after the layout operation completes. + * + * <h4>Default Cell Assignment</h4> + * + * If no child specifies the row and column indices of the cell it + * wishes to occupy, GridLayout assigns cell locations automatically using its: + * {@link GridLayout#setOrientation(int) orientation}, + * {@link GridLayout#setRowCount(int) rowCount} and + * {@link GridLayout#setColumnCount(int) columnCount} properties. + * + * <h4>Space</h4> + * + * Space between children may be specified either by using instances of the + * dedicated {@link Space} view or by setting the + * + * {@link ViewGroup.MarginLayoutParams#leftMargin leftMargin}, + * {@link ViewGroup.MarginLayoutParams#topMargin topMargin}, + * {@link ViewGroup.MarginLayoutParams#rightMargin rightMargin} and + * {@link ViewGroup.MarginLayoutParams#bottomMargin bottomMargin} + * + * layout parameters. When the + * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} + * property is set, default margins around children are automatically + * allocated based on the child's visual characteristics. Each of the + * margins so defined may be independently overridden by an assignment + * to the appropriate layout parameter. + * + * <h4>Excess Space Distribution</h4> + * + * Like {@link LinearLayout}, a child's ability to stretch is controlled + * using <em>weights</em>, which are specified using the + * {@link GridLayout.LayoutParams#rowWeight rowWeight} and + * {@link GridLayout.LayoutParams#columnWeight columnWeight} layout parameters. + * <p> + * <p> + * See {@link GridLayout.LayoutParams} for a full description of the + * layout parameters used by GridLayout. + * + * @attr ref android.R.styleable#GridLayout_orientation + * @attr ref android.R.styleable#GridLayout_rowCount + * @attr ref android.R.styleable#GridLayout_columnCount + * @attr ref android.R.styleable#GridLayout_useDefaultMargins + * @attr ref android.R.styleable#GridLayout_rowOrderPreserved + * @attr ref android.R.styleable#GridLayout_columnOrderPreserved + */ +public class GridLayout extends ViewGroup { + + // Public constants + + /** + * The horizontal orientation. + */ + public static final int HORIZONTAL = LinearLayout.HORIZONTAL; + + /** + * The vertical orientation. + */ + public static final int VERTICAL = LinearLayout.VERTICAL; + + /** + * The constant used to indicate that a value is undefined. + * Fields can use this value to indicate that their values + * have not yet been set. Similarly, methods can return this value + * to indicate that there is no suitable value that the implementation + * can return. + * The value used for the constant (currently {@link Integer#MIN_VALUE}) is + * intended to avoid confusion between valid values whose sign may not be known. + */ + public static final int UNDEFINED = Integer.MIN_VALUE; + + // Misc constants + + private static final String TAG = GridLayout.class.getName(); + private static final boolean DEBUG = false; + private static final Paint GRID_PAINT = new Paint(); + private static final double GOLDEN_RATIO = (1 + Math.sqrt(5)) / 2; + private static final int MIN = 0; + private static final int PRF = 1; + private static final int MAX = 2; + + // Defaults + + private static final int DEFAULT_ORIENTATION = HORIZONTAL; + private static final int DEFAULT_COUNT = UNDEFINED; + private static final boolean DEFAULT_USE_DEFAULT_MARGINS = false; + private static final boolean DEFAULT_ORDER_PRESERVED = false; + private static final boolean DEFAULT_MARGINS_INCLUDED = true; + // todo remove this + private static final int DEFAULT_CONTAINER_MARGIN = 20; + + // TypedArray indices + + private static final int ORIENTATION = styleable.GridLayout_orientation; + private static final int ROW_COUNT = styleable.GridLayout_rowCount; + private static final int COLUMN_COUNT = styleable.GridLayout_columnCount; + private static final int USE_DEFAULT_MARGINS = styleable.GridLayout_useDefaultMargins; + private static final int MARGINS_INCLUDED = styleable.GridLayout_marginsIncludedInAlignment; + private static final int ROW_ORDER_PRESERVED = styleable.GridLayout_rowOrderPreserved; + private static final int COLUMN_ORDER_PRESERVED = styleable.GridLayout_columnOrderPreserved; + + // Static initialization + + static { + GRID_PAINT.setColor(Color.argb(50, 255, 255, 255)); + } + + // Instance variables + + private final Axis mHorizontalAxis = new Axis(true); + private final Axis mVerticalAxis = new Axis(false); + private boolean mLayoutParamsValid = false; + private int mOrientation = DEFAULT_ORIENTATION; + private boolean mUseDefaultMargins = DEFAULT_USE_DEFAULT_MARGINS; + private boolean mMarginsIncludedInAlignment = DEFAULT_MARGINS_INCLUDED; + private int mDefaultGravity = Gravity.NO_GRAVITY; + + /* package */ boolean accommodateBothMinAndMax = false; + + // Constructors + + /** + * {@inheritDoc} + */ + public GridLayout(Context context) { + super(context); + if (DEBUG) { + setWillNotDraw(false); + } + } + + /** + * {@inheritDoc} + */ + public GridLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + processAttributes(context, attrs); + } + + /** + * {@inheritDoc} + */ + public GridLayout(Context context, AttributeSet attrs) { + super(context, attrs); + processAttributes(context, attrs); + } + + private void processAttributes(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, styleable.GridLayout); + try { + setRowCount(a.getInteger(ROW_COUNT, DEFAULT_COUNT)); + setColumnCount(a.getInteger(COLUMN_COUNT, DEFAULT_COUNT)); + mOrientation = a.getInteger(ORIENTATION, DEFAULT_ORIENTATION); + mUseDefaultMargins = a.getBoolean(USE_DEFAULT_MARGINS, DEFAULT_USE_DEFAULT_MARGINS); + mMarginsIncludedInAlignment = a.getBoolean(MARGINS_INCLUDED, DEFAULT_MARGINS_INCLUDED); + setRowOrderPreserved(a.getBoolean(ROW_ORDER_PRESERVED, DEFAULT_ORDER_PRESERVED)); + setColumnOrderPreserved(a.getBoolean(COLUMN_ORDER_PRESERVED, DEFAULT_ORDER_PRESERVED)); + } finally { + a.recycle(); + } + } + + // Implementation + + /** + * Returns the current orientation. + * + * @return either {@link #HORIZONTAL} or {@link #VERTICAL} + * + * @see #setOrientation(int) + * + * @attr ref android.R.styleable#GridLayout_orientation + */ + public int getOrientation() { + return mOrientation; + } + + /** + * The orientation property does not affect layout. Orientation is used + * only to generate default row/column indices when they are not specified + * by a component's layout parameters. + * <p> + * The default value of this property is {@link #HORIZONTAL}. + * + * @param orientation either {@link #HORIZONTAL} or {@link #VERTICAL} + * + * @see #getOrientation() + * + * @attr ref android.R.styleable#GridLayout_orientation + */ + public void setOrientation(int orientation) { + if (mOrientation != orientation) { + mOrientation = orientation; + requestLayout(); + } + } + + /** + * Returns the current number of rows. This is either the last value that was set + * with {@link #setRowCount(int)} or, if no such value was set, the maximum + * value of each the upper bounds defined in {@link LayoutParams#rowGroup}. + * + * @return the current number of rows + * + * @see #setRowCount(int) + * @see LayoutParams#rowGroup + * + * @attr ref android.R.styleable#GridLayout_rowCount + */ + public int getRowCount() { + return mVerticalAxis.getCount(); + } + + /** + * The rowCount property does not affect layout. RowCount is used + * only to generate default row/column indices when they are not specified + * by a component's layout parameters. + * + * @param rowCount the number of rows + * + * @see #getRowCount() + * @see LayoutParams#rowGroup + * + * @attr ref android.R.styleable#GridLayout_rowCount + */ + public void setRowCount(int rowCount) { + mVerticalAxis.setCount(rowCount); + } + + /** + * Returns the current number of columns. This is either the last value that was set + * with {@link #setColumnCount(int)} or, if no such value was set, the maximum + * value of each the upper bounds defined in {@link LayoutParams#columnGroup}. + * + * @return the current number of columns + * + * @see #setColumnCount(int) + * @see LayoutParams#columnGroup + * + * @attr ref android.R.styleable#GridLayout_columnCount + */ + public int getColumnCount() { + return mHorizontalAxis.getCount(); + } + + /** + * The columnCount property does not affect layout. ColumnCount is used + * only to generate default column/column indices when they are not specified + * by a component's layout parameters. + * + * @param columnCount the number of columns. + * + * @see #getColumnCount() + * @see LayoutParams#columnGroup + * + * @attr ref android.R.styleable#GridLayout_columnCount + */ + public void setColumnCount(int columnCount) { + mHorizontalAxis.setCount(columnCount); + } + + /** + * Returns whether or not this GridLayout will allocate default margins when no + * corresponding layout parameters are defined. + * + * @return {@code true} if default margins should be allocated + * + * @see #setUseDefaultMargins(boolean) + * + * @attr ref android.R.styleable#GridLayout_useDefaultMargins + */ + public boolean getUseDefaultMargins() { + return mUseDefaultMargins; + } + + /** + * When {@code true}, GridLayout allocates default margins around children + * based on the child's visual characteristics. Each of the + * margins so defined may be independently overridden by an assignment + * to the appropriate layout parameter. + * <p> + * When {@code false}, the default value of all margins is zero. + * <p> + * When setting to {@code true}, consider setting the value of the + * {@link #setMarginsIncludedInAlignment(boolean) marginsIncludedInAlignment} + * property to {@code false}. + * <p> + * The default value of this property is {@code false}. + * + * @param useDefaultMargins use {@code true} to make GridLayout allocate default margins + * + * @see #getUseDefaultMargins() + * @see #setMarginsIncludedInAlignment(boolean) + * + * @see MarginLayoutParams#leftMargin + * @see MarginLayoutParams#topMargin + * @see MarginLayoutParams#rightMargin + * @see MarginLayoutParams#bottomMargin + * + * @attr ref android.R.styleable#GridLayout_useDefaultMargins + */ + public void setUseDefaultMargins(boolean useDefaultMargins) { + mUseDefaultMargins = useDefaultMargins; + requestLayout(); + } + + /** + * Returns whether GridLayout aligns the edges of the view or the edges + * of the larger rectangle created by extending the view by its associated + * margins. + * + * @see #setMarginsIncludedInAlignment(boolean) + * + * @return {@code true} if alignment is between edges including margins + * + * @attr ref android.R.styleable#GridLayout_marginsIncludedInAlignment + */ + public boolean getMarginsIncludedInAlignment() { + return mMarginsIncludedInAlignment; + } + + /** + * When {@code true}, the bounds of a view are extended outwards according to its + * margins before the edges of the resulting rectangle are aligned. + * When {@code false}, alignment occurs between the bounds of the view - i.e. + * {@link #LEFT} alignment means align the left edges of the view. + * <p> + * The default value of this property is {@code true}. + * + * @param marginsIncludedInAlignment {@code true} if alignment between edges includes margins + * + * @see #getMarginsIncludedInAlignment() + * + * @attr ref android.R.styleable#GridLayout_marginsIncludedInAlignment + */ + public void setMarginsIncludedInAlignment(boolean marginsIncludedInAlignment) { + mMarginsIncludedInAlignment = marginsIncludedInAlignment; + requestLayout(); + } + + /** + * Returns whether or not row boundaries are ordered by their grid indices. + * + * @return {@code true} if row boundaries must appear in the order of their indices, + * {@code false} otherwise + * + * @see #setRowOrderPreserved(boolean) + * + * @attr ref android.R.styleable#GridLayout_rowOrderPreserved + */ + public boolean isRowOrderPreserved() { + return mVerticalAxis.isOrderPreserved(); + } + + /** + * When this property is {@code false}, the default state, GridLayout + * is at liberty to choose an order that better suits the heights of its children. + <p> + * When this property is {@code true}, GridLayout is forced to place the row boundaries + * so that their associated grid indices are in ascending order in the view. + * <p> + * GridLayout implements this specification by creating ordering constraints between + * the variables that represent the locations of the row boundaries. + * + * When this property is {@code true}, constraints are added for each pair of consecutive + * indices: i.e. between row boundaries: {@code [0..1], [1..2], [2..3],...} etc. + * + * When the property is {@code false}, the ordering constraints are placed + * only between boundaries that separate opposing edges of the layout's children. + * <p> + * The default value of this property is {@code false}. + + * @param rowOrderPreserved {@code true} to force GridLayout to respect the order + * of row boundaries + * + * @see #isRowOrderPreserved() + * + * @attr ref android.R.styleable#GridLayout_rowOrderPreserved + */ + public void setRowOrderPreserved(boolean rowOrderPreserved) { + mVerticalAxis.setOrderPreserved(rowOrderPreserved); + invalidateStructure(); + requestLayout(); + } + + /** + * Returns whether or not column boundaries are ordered by their grid indices. + * + * @return {@code true} if column boundaries must appear in the order of their indices, + * {@code false} otherwise + * + * @see #setColumnOrderPreserved(boolean) + * + * @attr ref android.R.styleable#GridLayout_columnOrderPreserved + */ + public boolean isColumnOrderPreserved() { + return mHorizontalAxis.isOrderPreserved(); + } + + /** + * When this property is {@code false}, the default state, GridLayout + * is at liberty to choose an order that better suits the widths of its children. + <p> + * When this property is {@code true}, GridLayout is forced to place the column boundaries + * so that their associated grid indices are in ascending order in the view. + * <p> + * GridLayout implements this specification by creating ordering constraints between + * the variables that represent the locations of the column boundaries. + * + * When this property is {@code true}, constraints are added for each pair of consecutive + * indices: i.e. between column boundaries: {@code [0..1], [1..2], [2..3],...} etc. + * + * When the property is {@code false}, the ordering constraints are placed + * only between boundaries that separate opposing edges of the layout's children. + * <p> + * The default value of this property is {@code false}. + * + * @param columnOrderPreserved use {@code true} to force GridLayout to respect the order + * of column boundaries. + * + * @see #isColumnOrderPreserved() + * + * @attr ref android.R.styleable#GridLayout_columnOrderPreserved + */ + public void setColumnOrderPreserved(boolean columnOrderPreserved) { + mHorizontalAxis.setOrderPreserved(columnOrderPreserved); + invalidateStructure(); + requestLayout(); + } + + private static int sum(float[] a) { + int result = 0; + for (int i = 0, length = a.length; i < length; i++) { + result += a[i]; + } + return result; + } + + private int getDefaultMargin(View c, boolean leading, boolean horizontal) { + // In the absence of any other information, calculate a default gap such + // that, in a grid of identical components, the heights and the vertical + // gaps are in the proportion of the golden ratio. + // To effect this with equal margins at each edge, set each of the + // four margin values to half this amount. + return (int) (c.getMeasuredHeight() / GOLDEN_RATIO / 2); + } + + private int getDefaultMargin(View c, boolean isAtEdge, boolean leading, boolean horizontal) { + // todo remove DEFAULT_CONTAINER_MARGIN. Use padding? Seek advice on Themes/Styles, etc. + return isAtEdge ? DEFAULT_CONTAINER_MARGIN : getDefaultMargin(c, leading, horizontal); + } + + private int getDefaultMarginValue(View c, LayoutParams p, boolean leading, boolean horizontal) { + if (!mUseDefaultMargins) { + return 0; + } + Group group = horizontal ? p.columnGroup : p.rowGroup; + Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis; + Interval span = group.span; + boolean isAtEdge = leading ? (span.min == 0) : (span.max == axis.getCount()); + + return getDefaultMargin(c, isAtEdge, leading, horizontal); + } + + private int getMargin(View view, boolean leading, boolean horizontal) { + LayoutParams lp = getLayoutParams(view); + int margin = horizontal ? + (leading ? lp.leftMargin : lp.rightMargin) : + (leading ? lp.topMargin : lp.bottomMargin); + return margin == UNDEFINED ? getDefaultMarginValue(view, lp, leading, horizontal) : margin; + } + + private static int valueIfDefined(int value, int defaultValue) { + return (value != UNDEFINED) ? value : defaultValue; + } + + // install default indices for cells that don't define them + private void validateLayoutParams() { + new Object() { + public int maxSize = 0; + + private int valueIfDefined2(int value, int defaultValue) { + if (value != UNDEFINED) { + maxSize = 0; + return value; + } else { + return defaultValue; + } + } + + { + final boolean horizontal = (mOrientation == HORIZONTAL); + final int axis = horizontal ? mHorizontalAxis.count : mVerticalAxis.count; + final int count = valueIfDefined(axis, Integer.MAX_VALUE); + + int row = 0; + int col = 0; + for (int i = 0, N = getChildCount(); i < N; i++) { + LayoutParams lp = getLayoutParams1(getChildAt(i)); + + Group colGroup = lp.columnGroup; + Interval cols = colGroup.span; + int colSpan = cols.size(); + + Group rowGroup = lp.rowGroup; + Interval rows = rowGroup.span; + int rowSpan = rows.size(); + + if (horizontal) { + row = valueIfDefined2(rows.min, row); + + int newCol = valueIfDefined(cols.min, (col + colSpan > count) ? 0 : col); + if (newCol < col) { + row += maxSize; + maxSize = 0; + } + col = newCol; + maxSize = max(maxSize, rowSpan); + } else { + col = valueIfDefined2(cols.min, col); + + int newRow = valueIfDefined(rows.min, (row + rowSpan > count) ? 0 : row); + if (newRow < row) { + col += maxSize; + maxSize = 0; + } + row = newRow; + maxSize = max(maxSize, colSpan); + } + + lp.setColumnGroupSpan(new Interval(col, col + colSpan)); + lp.setRowGroupSpan(new Interval(row, row + rowSpan)); + + if (horizontal) { + col = col + colSpan; + } else { + row = row + rowSpan; + } + } + } + }; + invalidateStructure(); + } + + private void invalidateStructure() { + mLayoutParamsValid = false; + mHorizontalAxis.invalidateStructure(); + mVerticalAxis.invalidateStructure(); + // This can end up being done twice. But better that than not at all. + invalidateValues(); + } + + private void invalidateValues() { + // Need null check because requestLayout() is called in View's initializer, + // before we are set up. + if (mHorizontalAxis != null && mVerticalAxis != null) { + mHorizontalAxis.invalidateValues(); + mVerticalAxis.invalidateValues(); + } + } + + private LayoutParams getLayoutParams1(View c) { + return (LayoutParams) c.getLayoutParams(); + } + + private LayoutParams getLayoutParams(View c) { + if (!mLayoutParamsValid) { + validateLayoutParams(); + mLayoutParamsValid = true; + } + return getLayoutParams1(c); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs, mDefaultGravity); + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return new LayoutParams(p); + } + + // Draw grid + + private void drawLine(Canvas graphics, int x1, int y1, int x2, int y2, Paint paint) { + int dx = getPaddingLeft(); + int dy = getPaddingTop(); + graphics.drawLine(dx + x1, dy + y1, dx + x2, dy + y2, paint); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (DEBUG) { + int height = getHeight() - getPaddingTop() - getPaddingBottom(); + int width = getWidth() - getPaddingLeft() - getPaddingRight(); + + int[] xs = mHorizontalAxis.locations; + for (int i = 0, length = xs.length; i < length; i++) { + int x = xs[i]; + drawLine(canvas, x, 0, x, height - 1, GRID_PAINT); + } + int[] ys = mVerticalAxis.locations; + for (int i = 0, length = ys.length; i < length; i++) { + int y = ys[i]; + drawLine(canvas, 0, y, width - 1, y, GRID_PAINT); + } + } + } + + // Add/remove + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + super.addView(child, index, params); + invalidateStructure(); + } + + @Override + public void removeView(View view) { + super.removeView(view); + invalidateStructure(); + } + + @Override + public void removeViewInLayout(View view) { + super.removeViewInLayout(view); + invalidateStructure(); + } + + @Override + public void removeViewsInLayout(int start, int count) { + super.removeViewsInLayout(start, count); + invalidateStructure(); + } + + @Override + public void removeViewAt(int index) { + super.removeViewAt(index); + invalidateStructure(); + } + + // Measurement + + private static int getChildMeasureSpec2(int spec, int padding, int childDimension) { + int resultSize; + int resultMode; + + if (childDimension >= 0) { + resultSize = childDimension; + resultMode = EXACTLY; + } else { + /* + using the following lines would replicate the logic of ViewGroup.getChildMeasureSpec() + + int specMode = MeasureSpec.getMode(spec); + int specSize = MeasureSpec.getSize(spec); + int size = Math.max(0, specSize - padding); + + resultSize = size; + resultMode = (specMode == EXACTLY && childDimension == LayoutParams.WRAP_CONTENT) ? + AT_MOST : specMode; + */ + resultSize = 0; + resultMode = UNSPECIFIED; + } + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } + + @Override + protected void measureChild(View child, int parentWidthSpec, int parentHeightSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + int childWidthMeasureSpec = getChildMeasureSpec2(parentWidthSpec, + mPaddingLeft + mPaddingRight, lp.width); + int childHeightMeasureSpec = getChildMeasureSpec2(parentHeightSpec, + mPaddingTop + mPaddingBottom, lp.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + measureChildren(widthSpec, heightSpec); + + int computedWidth = getPaddingLeft() + mHorizontalAxis.getMin() + getPaddingRight(); + int computedHeight = getPaddingTop() + mVerticalAxis.getMin() + getPaddingBottom(); + + setMeasuredDimension( + resolveSizeAndState(computedWidth, widthSpec, 0), + resolveSizeAndState(computedHeight, heightSpec, 0)); + } + + private int protect(int alignment) { + return (alignment == UNDEFINED) ? 0 : alignment; + } + + private int getMeasurement(View c, boolean horizontal, int measurementType) { + return horizontal ? c.getMeasuredWidth() : c.getMeasuredHeight(); + } + + private int getMeasurementIncludingMargin(View c, boolean horizontal, int measurementType) { + int result = getMeasurement(c, horizontal, measurementType); + if (mMarginsIncludedInAlignment) { + int leadingMargin = getMargin(c, true, horizontal); + int trailingMargin = getMargin(c, false, horizontal); + return result + leadingMargin + trailingMargin; + } + return result; + } + + @Override + public void requestLayout() { + super.requestLayout(); + invalidateValues(); + } + + // Layout container + + /** + * {@inheritDoc} + */ + /* + The layout operation is implemented by delegating the heavy lifting to the + to the mHorizontalAxis and mVerticalAxis instances of the internal Axis class. + Together they compute the locations of the vertical and horizontal lines of + the grid (respectively!). + + This method is then left with the simpler task of applying margins, gravity + and sizing to each child view and then placing it in its cell. + */ + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int targetWidth = r - l; + int targetHeight = b - t; + + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + + mHorizontalAxis.layout(targetWidth - paddingLeft - paddingRight); + mVerticalAxis.layout(targetHeight - paddingTop - paddingBottom); + + for (int i = 0, size = getChildCount(); i < size; i++) { + View view = getChildAt(i); + LayoutParams lp = getLayoutParams(view); + Group columnGroup = lp.columnGroup; + Group rowGroup = lp.rowGroup; + + Interval colSpan = columnGroup.span; + Interval rowSpan = rowGroup.span; + + int x1 = mHorizontalAxis.getLocationIncludingMargin(view, true, colSpan.min); + int y1 = mVerticalAxis.getLocationIncludingMargin(view, true, rowSpan.min); + + int x2 = mHorizontalAxis.getLocationIncludingMargin(view, false, colSpan.max); + int y2 = mVerticalAxis.getLocationIncludingMargin(view, false, rowSpan.max); + + int cellWidth = x2 - x1; + int cellHeight = y2 - y1; + + int pWidth = getMeasurement(view, true, PRF); + int pHeight = getMeasurement(view, false, PRF); + + Alignment hAlignment = columnGroup.alignment; + Alignment vAlignment = rowGroup.alignment; + + int dx, dy; + + Bounds colBounds = mHorizontalAxis.getGroupBounds().getValue(i); + Bounds rowBounds = mVerticalAxis.getGroupBounds().getValue(i); + + // Gravity offsets: the location of the alignment group relative to its cell group. + int c2ax = protect(hAlignment.getAlignmentValue(null, cellWidth - colBounds.size())); + int c2ay = protect(vAlignment.getAlignmentValue(null, cellHeight - rowBounds.size())); + + if (mMarginsIncludedInAlignment) { + int leftMargin = getMargin(view, true, true); + int topMargin = getMargin(view, true, false); + int rightMargin = getMargin(view, false, true); + int bottomMargin = getMargin(view, false, false); + + // Same calculation as getMeasurementIncludingMargin() + int measuredWidth = leftMargin + pWidth + rightMargin; + int measuredHeight = topMargin + pHeight + bottomMargin; + + // Alignment offsets: the location of the view relative to its alignment group. + int a2vx = colBounds.before - hAlignment.getAlignmentValue(view, measuredWidth); + int a2vy = rowBounds.before - vAlignment.getAlignmentValue(view, measuredHeight); + + dx = c2ax + a2vx + leftMargin; + dy = c2ay + a2vy + topMargin; + + cellWidth -= leftMargin + rightMargin; + cellHeight -= topMargin + bottomMargin; + } else { + // Alignment offsets: the location of the view relative to its alignment group. + int a2vx = colBounds.before - hAlignment.getAlignmentValue(view, pWidth); + int a2vy = rowBounds.before - vAlignment.getAlignmentValue(view, pHeight); + + dx = c2ax + a2vx; + dy = c2ay + a2vy; + } + + int width = hAlignment.getSizeInCell(view, pWidth, cellWidth); + int height = vAlignment.getSizeInCell(view, pHeight, cellHeight); + + int cx = paddingLeft + x1 + dx; + int cy = paddingTop + y1 + dy; + view.layout(cx, cy, cx + width, cy + height); + } + } + + // Inner classes + + /* + This internal class houses the algorithm for computing the locations of grid lines; + along either the horizontal or vertical axis. A GridLayout uses two instances of this class - + distinguished by the "horizontal" flag which is true for the horizontal axis and false + for the vertical one. + */ + private class Axis { + private static final int MIN_VALUE = -1000000; + + private static final int UNVISITED = 0; + private static final int PENDING = 1; + private static final int COMPLETE = 2; + + public final boolean horizontal; + + public int count = UNDEFINED; + public boolean countValid = false; + public boolean countWasExplicitySet = false; + + PackedMap<Group, Bounds> groupBounds; + public boolean groupBoundsValid = false; + + PackedMap<Interval, MutableInt> spanSizes; + public boolean spanSizesValid = false; + + public int[] leadingMargins; + public boolean leadingMarginsValid = false; + + public int[] trailingMargins; + public boolean trailingMarginsValid = false; + + public Arc[] arcs; + public boolean arcsValid = false; + + public int[] minima; + public boolean minimaValid = false; + + public float[] weights; + public int[] locations; + + private boolean mOrderPreserved = DEFAULT_ORDER_PRESERVED; + + private Axis(boolean horizontal) { + this.horizontal = horizontal; + } + + private int maxIndex() { + // note the number Integer.MIN_VALUE + 1 comes up in undefined cells + int count = -1; + for (int i = 0, size = getChildCount(); i < size; i++) { + LayoutParams params = getLayoutParams(getChildAt(i)); + Group g = horizontal ? params.columnGroup : params.rowGroup; + count = max(count, g.span.min); + count = max(count, g.span.max); + } + return count == -1 ? UNDEFINED : count; + } + + public int getCount() { + if (!countValid) { + count = max(0, maxIndex()); // if there are no cells, the count is zero + countValid = true; + } + return count; + } + + public void setCount(int count) { + this.count = count; + this.countWasExplicitySet = count != UNDEFINED; + } + + public boolean isOrderPreserved() { + return mOrderPreserved; + } + + public void setOrderPreserved(boolean orderPreserved) { + mOrderPreserved = orderPreserved; + invalidateStructure(); + } + + private PackedMap<Group, Bounds> createGroupBounds() { + int N = getChildCount(); + Group[] groups = new Group[N]; + Bounds[] bounds = new Bounds[N]; + for (int i = 0; i < N; i++) { + LayoutParams lp = getLayoutParams(getChildAt(i)); + Group group = horizontal ? lp.columnGroup : lp.rowGroup; + + groups[i] = group; + bounds[i] = new Bounds(); + } + + return new PackedMap<Group, Bounds>(groups, bounds); + } + + private void computeGroupBounds() { + for (int i = 0; i < groupBounds.values.length; i++) { + groupBounds.values[i].reset(); + } + for (int i = 0, N = getChildCount(); i < N; i++) { + View c = getChildAt(i); + LayoutParams lp = getLayoutParams(c); + Group g = horizontal ? lp.columnGroup : lp.rowGroup; + + Bounds bounds = groupBounds.getValue(i); + + int size = getMeasurementIncludingMargin(c, horizontal, PRF); + // todo test this works correctly when the returned value is UNDEFINED + int before = g.alignment.getAlignmentValue(c, size); + bounds.include(before, size - before); + } + } + + private PackedMap<Group, Bounds> getGroupBounds() { + if (groupBounds == null) { + groupBounds = createGroupBounds(); + } + if (!groupBoundsValid) { + computeGroupBounds(); + groupBoundsValid = true; + } + return groupBounds; + } + + // Add values computed by alignment - taking the max of all alignments in each span + private PackedMap<Interval, MutableInt> createSpanSizes() { + PackedMap<Group, Bounds> groupBounds = getGroupBounds(); + int N = groupBounds.keys.length; + Interval[] spans = new Interval[N]; + MutableInt[] values = new MutableInt[N]; + for (int i = 0; i < N; i++) { + Interval key = groupBounds.keys[i].span; + + spans[i] = key; + values[i] = new MutableInt(); + } + return new PackedMap<Interval, MutableInt>(spans, values); + } + + private void computeSpanSizes() { + MutableInt[] spans = spanSizes.values; + for (int i = 0; i < spans.length; i++) { + spans[i].reset(); + } + + Bounds[] bounds = getGroupBounds().values; // use getter to trigger a re-evaluation + for (int i = 0; i < bounds.length; i++) { + int value = bounds[i].size(); + + MutableInt valueHolder = spanSizes.getValue(i); + valueHolder.value = max(valueHolder.value, value); + } + } + + private PackedMap<Interval, MutableInt> getSpanSizes() { + if (spanSizes == null) { + spanSizes = createSpanSizes(); + } + if (!spanSizesValid) { + computeSpanSizes(); + spanSizesValid = true; + } + return spanSizes; + } + + private void include(List<Arc> arcs, Interval key, MutableInt size) { + // this bit below should really be computed outside here - + // its just to stop default (col>0) constraints obliterating valid entries + for (Arc arc : arcs) { + Interval span = arc.span; + if (span.equals(key)) { + return; + } + } + arcs.add(new Arc(key, size)); + } + + private void include2(List<Arc> arcs, Interval span, MutableInt min, MutableInt max, + boolean both) { + include(arcs, span, min); + if (both) { + // todo +// include(arcs, span.inverse(), max.neg()); + } + } + + private void include2(List<Arc> arcs, Interval span, int min, int max, boolean both) { + include2(arcs, span, new MutableInt(min), new MutableInt(max), both); + } + + // Group arcs by their first vertex, returning an array of arrays. + // This is linear in the number of arcs. + private Arc[][] groupArcsByFirstVertex(Arc[] arcs) { + int N = getCount() + 1;// the number of vertices + Arc[][] result = new Arc[N][]; + int[] sizes = new int[N]; + for (Arc arc : arcs) { + sizes[arc.span.min]++; + } + for (int i = 0; i < sizes.length; i++) { + result[i] = new Arc[sizes[i]]; + } + // reuse the sizes array to hold the current last elements as we insert each arc + Arrays.fill(sizes, 0); + for (Arc arc : arcs) { + int i = arc.span.min; + result[i][sizes[i]++] = arc; + } + + return result; + } + + /* + Topological sort. + */ + private Arc[] topologicalSort(final Arc[] arcs, int start) { + // todo ensure the <start> vertex is added in edge cases + final List<Arc> result = new ArrayList<Arc>(); + new Object() { + Arc[][] arcsByFirstVertex = groupArcsByFirstVertex(arcs); + int[] visited = new int[getCount() + 1]; + + boolean completesCycle(int loc) { + int state = visited[loc]; + if (state == UNVISITED) { + visited[loc] = PENDING; + for (Arc arc : arcsByFirstVertex[loc]) { + Interval span = arc.span; + // the recursive call + if (completesCycle(span.max)) { + // which arcs get set here is dependent on the order + // in which we explore nodes + arc.completesCycle = true; + } + result.add(arc); + } + visited[loc] = COMPLETE; + } else if (state == PENDING) { + return true; + } else if (state == COMPLETE) { + } + return false; + } + }.completesCycle(start); + Collections.reverse(result); + assert arcs.length == result.size(); + return result.toArray(new Arc[result.size()]); + } + + private boolean[] findUsed(Collection<Arc> arcs) { + boolean[] result = new boolean[getCount()]; + for (Arc arc : arcs) { + Interval span = arc.span; + int min = min(span.min, span.max); + int max = max(span.min, span.max); + for (int i = min; i < max; i++) { + result[i] = true; + } + } + return result; + } + + // todo unify with findUsed above. Both routines analyze which rows/columns are empty. + private Collection<Interval> getSpacers() { + List<Interval> result = new ArrayList<Interval>(); + int N = getCount() + 1; + int[] leadingEdgeCount = new int[N]; + int[] trailingEdgeCount = new int[N]; + for (int i = 0, size = getChildCount(); i < size; i++) { + LayoutParams lp = getLayoutParams(getChildAt(i)); + Group g = horizontal ? lp.columnGroup : lp.rowGroup; + Interval span = g.span; + leadingEdgeCount[span.min]++; + trailingEdgeCount[span.max]++; + } + + int lastTrailingEdge = 0; + + // treat the parent's edges like peer edges of the opposite type + trailingEdgeCount[0] = 1; + leadingEdgeCount[N - 1] = 1; + + for (int i = 0; i < N; i++) { + if (trailingEdgeCount[i] > 0) { + lastTrailingEdge = i; + continue; // if this is also a leading edge, don't add a space of length zero + } + if (leadingEdgeCount[i] > 0) { + result.add(new Interval(lastTrailingEdge, i)); + } + } + return result; + } + + private Arc[] createArcs() { + List<Arc> spanToSize = new ArrayList<Arc>(); + + // Add all the preferred elements that were not defined by the user. + PackedMap<Interval, MutableInt> spanSizes = getSpanSizes(); + for (int i = 0; i < spanSizes.keys.length; i++) { + Interval key = spanSizes.keys[i]; + MutableInt value = spanSizes.values[i]; + // todo remove value duplicate + include2(spanToSize, key, value, value, accommodateBothMinAndMax); + } + + // Find redundant rows/cols and glue them together with 0-length arcs to link the tree + boolean[] used = findUsed(spanToSize); + for (int i = 0; i < getCount(); i++) { + if (!used[i]) { + Interval span = new Interval(i, i + 1); + include(spanToSize, span, new MutableInt(0)); + include(spanToSize, span.inverse(), new MutableInt(0)); + } + } + + if (mOrderPreserved) { + // Add preferred gaps + for (int i = 0; i < getCount(); i++) { + if (used[i]) { + include2(spanToSize, new Interval(i, i + 1), 0, 0, false); + } + } + } else { + for (Interval gap : getSpacers()) { + include2(spanToSize, gap, 0, 0, false); + } + } + Arc[] arcs = spanToSize.toArray(new Arc[spanToSize.size()]); + return topologicalSort(arcs, 0); + } + + public Arc[] getArcs() { + if (arcs == null) { + arcs = createArcs(); + } + if (!arcsValid) { + getSpanSizes(); + arcsValid = true; + } + return arcs; + } + + private boolean relax(int[] locations, Arc entry) { + Interval span = entry.span; + int u = span.min; + int v = span.max; + int value = entry.value.value; + int candidate = locations[u] + value; + if (candidate > locations[v]) { + locations[v] = candidate; + return true; + } + return false; + } + + /* + Bellman-Ford variant - modified to reduce typical running time from O(N^2) to O(N) + + GridLayout converts its requirements into a system of linear constraints of the + form: + + x[i] - x[j] < a[k] + + Where the x[i] are variables and the a[k] are constants. + + For example, if the variables were instead labeled x, y, z we might have: + + x - y < 17 + y - z < 23 + z - x < 42 + + This is a special case of the Linear Programming problem that is, in turn, + equivalent to the single-source shortest paths problem on a digraph, for + which the O(n^2) Bellman-Ford algorithm the most commonly used general solution. + + Other algorithms are faster in the case where no arcs have negative weights + but allowing negative weights turns out to be the same as accommodating maximum + size requirements as well as minimum ones. + + Bellman-Ford works by iteratively 'relaxing' constraints over all nodes (an O(N) + process) and performing this step N times. Proof of correctness hinges on the + fact that there can be no negative weight chains of length > N - unless a + 'negative weight loop' exists. The algorithm catches this case in a final + checking phase that reports failure. + + By topologically sorting the nodes and checking this condition at each step + typical layout problems complete after the first iteration and the algorithm + completes in O(N) steps with very low constants. + */ + private int[] solve(Arc[] arcs, int[] locations) { + int N = getCount() + 1; // The number of vertices is the number of columns/rows + 1. + + boolean changed = false; + // We take one extra pass over traditional Bellman-Ford (and omit their final step) + for (int i = 0; i < N; i++) { + changed = false; + for (int j = 0, length = arcs.length; j < length; j++) { + changed = changed | relax(locations, arcs[j]); + } + if (!changed) { + if (DEBUG) { + Log.d(TAG, "Iteration " + + " completed after " + (1 + i) + " steps out of " + N); + } + break; + } + } + if (changed) { + Log.d(TAG, "*** Algorithm failed to terminate ***"); + } + return locations; + } + + private void computeMargins(boolean leading) { + int[] margins = leading ? leadingMargins : trailingMargins; + for (int i = 0, size = getChildCount(); i < size; i++) { + View c = getChildAt(i); + LayoutParams lp = getLayoutParams(c); + Group g = horizontal ? lp.columnGroup : lp.rowGroup; + Interval span = g.span; + int index = leading ? span.min : span.max; + margins[index] = max(margins[index], getMargin(c, leading, horizontal)); + } + } + + private int[] getLeadingMargins() { + if (leadingMargins == null) { + leadingMargins = new int[getCount() + 1]; + } + if (!leadingMarginsValid) { + computeMargins(true); + leadingMarginsValid = true; + } + return leadingMargins; + } + + private int[] getTrailingMargins() { + if (trailingMargins == null) { + trailingMargins = new int[getCount() + 1]; + } + if (!trailingMarginsValid) { + computeMargins(false); + trailingMarginsValid = true; + } + return trailingMargins; + } + + private void addMargins() { + int[] leadingMargins = getLeadingMargins(); + int[] trailingMargins = getTrailingMargins(); + + int delta = 0; + for (int i = 0, N = getCount(); i < N; i++) { + int margins = leadingMargins[i] + trailingMargins[i + 1]; + delta += margins; + minima[i + 1] += delta; + } + } + + private int getLocationIncludingMargin(View view, boolean leading, int index) { + int location = locations[index]; + int margin; + if (!mMarginsIncludedInAlignment) { + margin = (leading ? leadingMargins : trailingMargins)[index]; + } else { + margin = 0; + } + return leading ? (location + margin) : (location - margin); + } + + private void computeMinima(int[] a) { + Arrays.fill(a, MIN_VALUE); + a[0] = 0; + solve(getArcs(), a); + if (!mMarginsIncludedInAlignment) { + addMargins(); + } + } + + private int[] getMinima() { + if (minima == null) { + int N = getCount() + 1; + minima = new int[N]; + } + if (!minimaValid) { + computeMinima(minima); + minimaValid = true; + } + return minima; + } + + private void computeWeights() { + for (int i = 0, N = getChildCount(); i < N; i++) { + LayoutParams lp = getLayoutParams(getChildAt(i)); + Group g = horizontal ? lp.columnGroup : lp.rowGroup; + Interval span = g.span; + int penultimateIndex = span.max - 1; + weights[penultimateIndex] += horizontal ? lp.columnWeight : lp.rowWeight; + } + } + + private float[] getWeights() { + if (weights == null) { + int N = getCount(); + weights = new float[N]; + } + computeWeights(); + return weights; + } + + private int[] getLocations() { + if (locations == null) { + int N = getCount() + 1; + locations = new int[N]; + } + return locations; + } + + // External entry points + + private int size(int[] locations) { + return locations[locations.length - 1] - locations[0]; + } + + private int getMin() { + return size(getMinima()); + } + + private void layout(int targetSize) { + int[] mins = getMinima(); + + int totalDelta = max(0, targetSize - size(mins)); // confine to expansion + + float[] weights = getWeights(); + float totalWeight = sum(weights); + + if (totalWeight == 0f && weights.length > 0) { + weights[weights.length - 1] = 1; + totalWeight = 1; + } + + int[] locations = getLocations(); + int cumulativeDelta = 0; + + // note |weights| = |locations| - 1 + for (int i = 0; i < weights.length; i++) { + float weight = weights[i]; + int delta = (int) (totalDelta * weight / totalWeight); + cumulativeDelta += delta; + locations[i + 1] = mins[i + 1] + cumulativeDelta; + + totalDelta -= delta; + totalWeight -= weight; + } + } + + private void invalidateStructure() { + countValid = false; + + groupBounds = null; + spanSizes = null; + leadingMargins = null; + trailingMargins = null; + minima = null; + weights = null; + locations = null; + + invalidateValues(); + } + + private void invalidateValues() { + groupBoundsValid = false; + spanSizesValid = false; + arcsValid = false; + leadingMarginsValid = false; + trailingMarginsValid = false; + minimaValid = false; + } + } + + /** + * Layout information associated with each of the children of a GridLayout. + * <p> + * GridLayout supports both row and column spanning and arbitrary forms of alignment within + * each cell group. The fundamental parameters associated with each cell group are + * gathered into their vertical and horizontal components and stored + * in the {@link #rowGroup} and {@link #columnGroup} layout parameters. + * {@link Group Groups} are immutable structures and may be shared between the layout + * parameters of different children. + * <p> + * The row and column groups contain the leading and trailing indices along each axis + * and together specify the four grid indices that delimit the cells of this cell group. + * <p> + * The {@link Group#alignment alignment} fields of the row and column groups together specify + * both aspects of alignment within the cell group. It is also possible to specify a child's + * alignment within its cell group by using the {@link GridLayout.LayoutParams#setGravity(int)} + * method. + * <p> + * See {@link GridLayout} for a description of the conventions used by GridLayout + * in reference to grid indices. + * + * <h4>Default values</h4> + * + * <ul> + * <li>{@link #width} = {@link #WRAP_CONTENT}</li> + * <li>{@link #height} = {@link #WRAP_CONTENT}</li> + * <li>{@link #topMargin} = 0 when + * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is + * {@code false}; otherwise {@link #UNDEFINED}, to + * indicate that a default value should be computed on demand. </li> + * <li>{@link #leftMargin} = 0 when + * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is + * {@code false}; otherwise {@link #UNDEFINED}, to + * indicate that a default value should be computed on demand. </li> + * <li>{@link #bottomMargin} = 0 when + * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is + * {@code false}; otherwise {@link #UNDEFINED}, to + * indicate that a default value should be computed on demand. </li> + * <li>{@link #rightMargin} = 0 when + * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is + * {@code false}; otherwise {@link #UNDEFINED}, to + * indicate that a default value should be computed on demand. </li> + * <li>{@link #rowGroup}{@code .span} = {@code [0, 1]} </li> + * <li>{@link #rowGroup}{@code .alignment} = {@link #BASELINE} </li> + * <li>{@link #columnGroup}{@code .span} = {@code [0, 1]} </li> + * <li>{@link #columnGroup}{@code .alignment} = {@link #LEFT} </li> + * <li>{@link #rowWeight} = {@code 0f} </li> + * <li>{@link #columnWeight} = {@code 0f} </li> + * </ul> + * + * @attr ref android.R.styleable#GridLayout_Layout_layout_row + * @attr ref android.R.styleable#GridLayout_Layout_layout_rowSpan + * @attr ref android.R.styleable#GridLayout_Layout_layout_rowWeight + * @attr ref android.R.styleable#GridLayout_Layout_layout_column + * @attr ref android.R.styleable#GridLayout_Layout_layout_columnSpan + * @attr ref android.R.styleable#GridLayout_Layout_layout_columnWeight + * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity + */ + public static class LayoutParams extends MarginLayoutParams { + + // Default values + + private static final int DEFAULT_WIDTH = WRAP_CONTENT; + private static final int DEFAULT_HEIGHT = WRAP_CONTENT; + private static final int DEFAULT_MARGIN = UNDEFINED; + private static final int DEFAULT_ROW = UNDEFINED; + private static final int DEFAULT_COLUMN = UNDEFINED; + private static final Interval DEFAULT_SPAN = new Interval(UNDEFINED, UNDEFINED + 1); + private static final int DEFAULT_SPAN_SIZE = DEFAULT_SPAN.size(); + private static final Alignment DEFAULT_COLUMN_ALIGNMENT = LEFT; + private static final Alignment DEFAULT_ROW_ALIGNMENT = BASELINE; + private static final Group DEFAULT_COLUMN_GROUP = + new Group(DEFAULT_SPAN, DEFAULT_COLUMN_ALIGNMENT); + private static final Group DEFAULT_ROW_GROUP = + new Group(DEFAULT_SPAN, DEFAULT_ROW_ALIGNMENT); + private static final int DEFAULT_WEIGHT_0 = 0; + private static final int DEFAULT_WEIGHT_1 = 1; + + // Misc + + private static final Rect CONTAINER_BOUNDS = new Rect(0, 0, 2, 2); + private static final Alignment[] COLUMN_ALIGNMENTS = { LEFT, CENTER, RIGHT }; + private static final Alignment[] ROW_ALIGNMENTS = { TOP, CENTER, BOTTOM }; + + // TypedArray indices + + private static final int MARGIN = styleable.ViewGroup_MarginLayout_layout_margin; + private static final int LEFT_MARGIN = styleable.ViewGroup_MarginLayout_layout_marginLeft; + private static final int TOP_MARGIN = styleable.ViewGroup_MarginLayout_layout_marginTop; + private static final int RIGHT_MARGIN = styleable.ViewGroup_MarginLayout_layout_marginRight; + private static final int BOTTOM_MARGIN = + styleable.ViewGroup_MarginLayout_layout_marginBottom; + + private static final int COLUMN = styleable.GridLayout_Layout_layout_column; + private static final int COLUMN_SPAN = styleable.GridLayout_Layout_layout_columnSpan; + private static final int COLUMN_WEIGHT = styleable.GridLayout_Layout_layout_columnWeight; + private static final int ROW = styleable.GridLayout_Layout_layout_row; + private static final int ROW_SPAN = styleable.GridLayout_Layout_layout_rowSpan; + private static final int ROW_WEIGHT = styleable.GridLayout_Layout_layout_rowWeight; + private static final int GRAVITY = styleable.GridLayout_Layout_layout_gravity; + + // Instance variables + + /** + * The group that specifies the vertical characteristics of the cell group + * described by these layout parameters. + */ + public Group rowGroup; + /** + * The group that specifies the horizontal characteristics of the cell group + * described by these layout parameters. + */ + public Group columnGroup; + /** + * The proportional space that should be taken by the associated row group + * during excess space distribution. + */ + public float rowWeight; + /** + * The proportional space that should be taken by the associated column group + * during excess space distribution. + */ + public float columnWeight; + + // Constructors + + private LayoutParams( + int width, int height, + int left, int top, int right, int bottom, + Group rowGroup, Group columnGroup, float rowWeight, float columnWeight) { + super(width, height); + setMargins(left, top, right, bottom); + this.rowGroup = rowGroup; + this.columnGroup = columnGroup; + this.rowWeight = rowWeight; + this.columnWeight = columnWeight; + } + + /** + * Constructs a new LayoutParams instance for this <code>rowGroup</code> + * and <code>columnGroup</code>. All other fields are initialized with + * default values as defined in {@link LayoutParams}. + * + * @param rowGroup the rowGroup + * @param columnGroup the columnGroup + */ + public LayoutParams(Group rowGroup, Group columnGroup) { + this(DEFAULT_WIDTH, DEFAULT_HEIGHT, + DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, + rowGroup, columnGroup, DEFAULT_WEIGHT_0, DEFAULT_WEIGHT_0); + } + + /** + * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}. + */ + public LayoutParams() { + this(DEFAULT_ROW_GROUP, DEFAULT_COLUMN_GROUP); + } + + // Copying constructors + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams params) { + super(params); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(MarginLayoutParams params) { + super(params); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(LayoutParams that) { + super(that); + this.columnGroup = that.columnGroup; + this.rowGroup = that.rowGroup; + this.columnWeight = that.columnWeight; + this.rowWeight = that.rowWeight; + } + + // AttributeSet constructors + + private LayoutParams(Context context, AttributeSet attrs, int defaultGravity) { + super(context, attrs); + reInitSuper(context, attrs); + init(context, attrs, defaultGravity); + } + + /** + * {@inheritDoc} + * + * Values not defined in the attribute set take the default values + * defined in {@link LayoutParams}. + */ + public LayoutParams(Context context, AttributeSet attrs) { + this(context, attrs, Gravity.NO_GRAVITY); + } + + // Implementation + + private static boolean definesVertical(int gravity) { + return gravity > 0 && (gravity & Gravity.VERTICAL_GRAVITY_MASK) != 0; + } + + private static boolean definesHorizontal(int gravity) { + return gravity > 0 && (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != 0; + } + + private static <T> T getAlignment(T[] alignments, T fill, int min, int max, + boolean isUndefined, T defaultValue) { + if (isUndefined) { + return defaultValue; + } + return min != max ? fill : alignments[min]; + } + + // Reinitialise the margins using a different default policy than MarginLayoutParams. + // Here we use the value UNDEFINED (as distinct from zero) to represent the undefined state + // so that a layout manager default can be accessed post set up. We need this as, at the + // point of installation, we do not know how many rows/cols there are and therefore + // which elements are positioned next to the container's trailing edges. We need to + // know this as margins around the container's boundary should have different + // defaults to those between peers. + + // This method could be parametrized and moved into MarginLayout. + private void reInitSuper(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, styleable.ViewGroup_MarginLayout); + try { + int margin = a.getDimensionPixelSize(MARGIN, DEFAULT_MARGIN); + + this.leftMargin = a.getDimensionPixelSize(LEFT_MARGIN, margin); + this.topMargin = a.getDimensionPixelSize(TOP_MARGIN, margin); + this.rightMargin = a.getDimensionPixelSize(RIGHT_MARGIN, margin); + this.bottomMargin = a.getDimensionPixelSize(BOTTOM_MARGIN, margin); + } finally { + a.recycle(); + } + } + + // Gravity. For conversion from the static the integers defined in the Gravity class, + // use Gravity.apply() to apply gravity to a view of zero size and see where it ends up. + private static Alignment getColumnAlignment(int gravity, int width) { + Rect r = new Rect(0, 0, 0, 0); + Gravity.apply(gravity, 0, 0, CONTAINER_BOUNDS, r); + + boolean fill = (width == MATCH_PARENT); + Alignment defaultAlignment = fill ? FILL : DEFAULT_COLUMN_ALIGNMENT; + return getAlignment(COLUMN_ALIGNMENTS, FILL, r.left, r.right, + !definesHorizontal(gravity), defaultAlignment); + } + + private static Alignment getRowAlignment(int gravity, int height) { + Rect r = new Rect(0, 0, 0, 0); + Gravity.apply(gravity, 0, 0, CONTAINER_BOUNDS, r); + + boolean fill = (height == MATCH_PARENT); + Alignment defaultAlignment = fill ? FILL : DEFAULT_ROW_ALIGNMENT; + return getAlignment(ROW_ALIGNMENTS, FILL, r.top, r.bottom, + !definesVertical(gravity), defaultAlignment); + } + + private int getDefaultWeight(int size) { + return (size == MATCH_PARENT) ? DEFAULT_WEIGHT_1 : DEFAULT_WEIGHT_0; + } + + private void init(Context context, AttributeSet attrs, int defaultGravity) { + TypedArray a = context.obtainStyledAttributes(attrs, styleable.GridLayout_Layout); + try { + int gravity = a.getInteger(GRAVITY, defaultGravity); + + int column = a.getInteger(COLUMN, DEFAULT_COLUMN); + int columnSpan = a.getInteger(COLUMN_SPAN, DEFAULT_SPAN_SIZE); + Interval hSpan = new Interval(column, column + columnSpan); + this.columnGroup = new Group(hSpan, getColumnAlignment(gravity, width)); + this.columnWeight = a.getFloat(COLUMN_WEIGHT, getDefaultWeight(width)); + + int row = a.getInteger(ROW, DEFAULT_ROW); + int rowSpan = a.getInteger(ROW_SPAN, DEFAULT_SPAN_SIZE); + Interval vSpan = new Interval(row, row + rowSpan); + this.rowGroup = new Group(vSpan, getRowAlignment(gravity, height)); + this.rowWeight = a.getFloat(ROW_WEIGHT, getDefaultWeight(height)); + } finally { + a.recycle(); + } + } + + /** + * Describes how the child views are positioned. Default is {@code LEFT | BASELINE}. + * See {@link android.view.Gravity}. + * + * @param gravity the new gravity value + * + * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity + */ + public void setGravity(int gravity) { + columnGroup = columnGroup.copyWriteAlignment(getColumnAlignment(gravity, width)); + rowGroup = rowGroup.copyWriteAlignment(getRowAlignment(gravity, height)); + } + + @Override + protected void setBaseAttributes(TypedArray attributes, int widthAttr, int heightAttr) { + this.width = attributes.getLayoutDimension(widthAttr, DEFAULT_WIDTH); + this.height = attributes.getLayoutDimension(heightAttr, DEFAULT_HEIGHT); + } + + private void setRowGroupSpan(Interval span) { + rowGroup = rowGroup.copyWriteSpan(span); + } + + private void setColumnGroupSpan(Interval span) { + columnGroup = columnGroup.copyWriteSpan(span); + } + } + + /* + In place of a HashMap from span to Int, use an array of key/value pairs - stored in Arcs. + Add the mutables completesCycle flag to avoid creating another hash table for detecting cycles. + */ + private static class Arc { + public final Interval span; + public final MutableInt value; + public boolean completesCycle; + + public Arc(Interval span, MutableInt value) { + this.span = span; + this.value = value; + } + + @Override + public String toString() { + return span + " " + (completesCycle ? "+>" : "->") + " " + value; + } + } + + // A mutable Integer - used to avoid heap allocation during the layout operation + + private static class MutableInt { + public int value; + + private MutableInt() { + reset(); + } + + private MutableInt(int value) { + this.value = value; + } + + private void reset() { + value = Integer.MIN_VALUE; + } + } + + /* + This data structure is used in place of a Map where we have an index that refers to the order + in which each key/value pairs were added to the map. In this case we store keys and values + in arrays of a length that is equal to the number of unique keys. We also maintain an + array of indexes from insertion order to the compacted arrays of keys and values. + + Note that behavior differs from that of a LinkedHashMap in that repeated entries + *do* get added multiples times. So the length of index is equals to the number of + items added. + + This is useful in the GridLayout class where we can rely on the order of children not + changing during layout - to use integer-based lookup for our internal structures + rather than using (and storing) an implementation of Map<Key, ?>. + */ + @SuppressWarnings(value = "unchecked") + private static class PackedMap<K, V> { + public final int[] index; + public final K[] keys; + public final V[] values; + + private PackedMap(K[] keys, V[] values) { + this.index = createIndex(keys); + + this.keys = compact(keys, index); + this.values = compact(values, index); + } + + private K getKey(int i) { + return keys[index[i]]; + } + + private V getValue(int i) { + return values[index[i]]; + } + + private static <K> int[] createIndex(K[] keys) { + int size = keys.length; + int[] result = new int[size]; + + Map<K, Integer> keyToIndex = new HashMap<K, Integer>(); + for (int i = 0; i < size; i++) { + K key = keys[i]; + Integer index = keyToIndex.get(key); + if (index == null) { + index = keyToIndex.size(); + keyToIndex.put(key, index); + } + result[i] = index; + } + return result; + } + + private static int max(int[] a, int valueIfEmpty) { + int result = valueIfEmpty; + for (int i = 0, length = a.length; i < length; i++) { + result = Math.max(result, a[i]); + } + return result; + } + + /* + Create a compact array of keys or values using the supplied index. + */ + private static <K> K[] compact(K[] a, int[] index) { + int size = a.length; + Class<?> componentType = a.getClass().getComponentType(); + K[] result = (K[]) Array.newInstance(componentType, max(index, -1) + 1); + + // this overwrite duplicates, retaining the last equivalent entry + for (int i = 0; i < size; i++) { + result[index[i]] = a[i]; + } + return result; + } + } + + /* + For each Group (with a given alignment) we need to store the amount of space required + before the alignment point and the amount of space required after it. One side of this + calculation is always 0 for LEADING and TRAILING alignments but we don't make use of this. + For CENTER and BASELINE alignments both sides are needed and in the BASELINE case no + simple optimisations are possible. + + The general algorithm therefore is to create a Map (actually a PackedMap) from + Group to Bounds and to loop through all Views in the group taking the maximum + of the values for each View. + */ + private static class Bounds { + public int before; + public int after; + + private Bounds() { + reset(); + } + + private void reset() { + before = Integer.MIN_VALUE; + after = Integer.MIN_VALUE; + } + + private void include(int before, int after) { + this.before = max(this.before, before); + this.after = max(this.after, after); + } + + private int size() { + return before + after; + } + + @Override + public String toString() { + return "Bounds{" + + "before=" + before + + ", after=" + after + + '}'; + } + } + + /** + * An Interval represents a contiguous range of values that lie between + * the interval's {@link #min} and {@link #max} values. + * <p> + * Intervals are immutable so may be passed as values and used as keys in hash tables. + * It is not necessary to have multiple instances of Intervals which have the same + * {@link #min} and {@link #max} values. + * <p> + * Intervals are often written as {@code [min, max]} and represent the set of values + * {@code x} such that {@code min <= x < max}. + */ + /* package */ static class Interval { + /** + * The minimum value. + */ + public final int min; + + /** + * The maximum value. + */ + public final int max; + + /** + * Construct a new Interval, {@code interval}, where: + * <ul> + * <li> {@code interval.min = min} </li> + * <li> {@code interval.max = max} </li> + * </ul> + * + * @param min the minimum value. + * @param max the maximum value. + */ + public Interval(int min, int max) { + this.min = min; + this.max = max; + } + + private int size() { + return max - min; + } + + private Interval inverse() { + return new Interval(max, min); + } + + /** + * Returns {@code true} if the {@link #getClass class}, + * {@link #min} and {@link #max} properties of this Interval and the + * supplied parameter are pairwise equal; {@code false} otherwise. + * + * @param that the object to compare this interval with + * + * @return {@code true} if the specified object is equal to this + * {@code Interval}, {@code false} otherwise. + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + if (that == null || getClass() != that.getClass()) { + return false; + } + + Interval interval = (Interval) that; + + if (max != interval.max) { + return false; + } + if (min != interval.min) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = min; + result = 31 * result + max; + return result; + } + + @Override + public String toString() { + return "[" + min + ", " + max + "]"; + } + } + + /** + * A group specifies either the horizontal or vertical characteristics of a group of + * cells. + * <p> + * Groups are immutable and so may be shared between views with the same + * {@code span} and {@code alignment}. + */ + public static class Group { + /** + * The grid indices of the leading and trailing edges of this cell group for the + * appropriate axis. + * <p> + * See {@link GridLayout} for a description of the conventions used by GridLayout + * for grid indices. + */ + /* package */ final Interval span; + /** + * Specifies how cells should be aligned in this group. + * For row groups, this specifies the vertical alignment. + * For column groups, this specifies the horizontal alignment. + */ + public final Alignment alignment; + + /** + * Construct a new Group, {@code group}, where: + * <ul> + * <li> {@code group.span = span} </li> + * <li> {@code group.alignment = alignment} </li> + * </ul> + * + * @param span the span + * @param alignment the alignment + */ + /* package */ Group(Interval span, Alignment alignment) { + this.span = span; + this.alignment = alignment; + } + + /** + * Construct a new Group, {@code group}, where: + * <ul> + * <li> {@code group.span = [start, start + size]} </li> + * <li> {@code group.alignment = alignment} </li> + * </ul> + * + * @param start the start + * @param size the size + * @param alignment the alignment + */ + public Group(int start, int size, Alignment alignment) { + this(new Interval(start, start + size), alignment); + } + + /** + * Construct a new Group, {@code group}, where: + * <ul> + * <li> {@code group.span = [start, start + 1]} </li> + * <li> {@code group.alignment = alignment} </li> + * </ul> + * + * @param start the start index + * @param alignment the alignment + */ + public Group(int start, Alignment alignment) { + this(start, 1, alignment); + } + + private Group copyWriteSpan(Interval span) { + return new Group(span, alignment); + } + + private Group copyWriteAlignment(Alignment alignment) { + return new Group(span, alignment); + } + + /** + * Returns {@code true} if the {@link #getClass class}, {@link #alignment} and {@code span} + * properties of this Group and the supplied parameter are pairwise equal, + * {@code false} otherwise. + * + * @param that the object to compare this group with + * + * @return {@code true} if the specified object is equal to this + * {@code Group}; {@code false} otherwise + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + if (that == null || getClass() != that.getClass()) { + return false; + } + + Group group = (Group) that; + + if (!alignment.equals(group.alignment)) { + return false; + } + if (!span.equals(group.span)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = span.hashCode(); + result = 31 * result + alignment.hashCode(); + return result; + } + } + + /** + * Alignments specify where a view should be placed within a cell group and + * what size it should be. + * <p> + * The {@link LayoutParams} class contains a {@link LayoutParams#rowGroup rowGroup} + * and a {@link LayoutParams#columnGroup columnGroup} each of which contains an + * {@link Group#alignment alignment}. Overall placement of the view in the cell + * group is specified by the two alignments which act along each axis independently. + * <p> + * An Alignment implementation must define the {@link #getAlignmentValue(View, int)} + * to return the appropriate value for the type of alignment being defined. + * The enclosing algorithms position the children + * so that the values returned from the alignment + * are the same for all of the views in a group. + * <p> + * The GridLayout class defines the most common alignments used in general layout: + * {@link #TOP}, {@link #LEFT}, {@link #BOTTOM}, {@link #RIGHT}, {@link #CENTER}, {@link + * #BASELINE} and {@link #FILL}. + */ + public static interface Alignment { + /** + * Returns an alignment value. In the case of vertical alignments the value + * returned should indicate the distance from the top of the view to the + * alignment location. + * For horizontal alignments measurement is made from the left edge of the component. + * + * @param view the view to which this alignment should be applied + * @param viewSize the measured size of the view + * @return the alignment value + */ + public int getAlignmentValue(View view, int viewSize); + + /** + * Returns the size of the view specified by this alignment. + * In the case of vertical alignments this method should return a height; for + * horizontal alignments this method should return the width. + * + * @param view the view to which this alignment should be applied + * @param viewSize the measured size of the view + * @param cellSize the size of the cell into which this view will be placed + * @return the aligned size + */ + public int getSizeInCell(View view, int viewSize, int cellSize); + } + + private static abstract class AbstractAlignment implements Alignment { + public int getSizeInCell(View view, int viewSize, int cellSize) { + return viewSize; + } + } + + private static final Alignment LEADING = new AbstractAlignment() { + public int getAlignmentValue(View view, int viewSize) { + return 0; + } + + }; + + private static final Alignment TRAILING = new AbstractAlignment() { + public int getAlignmentValue(View view, int viewSize) { + return viewSize; + } + }; + + /** + * Indicates that a view should be aligned with the <em>top</em> + * edges of the other views in its cell group. + */ + public static final Alignment TOP = LEADING; + + /** + * Indicates that a view should be aligned with the <em>bottom</em> + * edges of the other views in its cell group. + */ + public static final Alignment BOTTOM = TRAILING; + + /** + * Indicates that a view should be aligned with the <em>right</em> + * edges of the other views in its cell group. + */ + public static final Alignment RIGHT = TRAILING; + + /** + * Indicates that a view should be aligned with the <em>left</em> + * edges of the other views in its cell group. + */ + public static final Alignment LEFT = LEADING; + + /** + * Indicates that a view should be <em>centered</em> with the other views in its cell group. + * This constant may be used in both {@link LayoutParams#rowGroup rowGroups} and {@link + * LayoutParams#columnGroup columnGroups}. + */ + public static final Alignment CENTER = new AbstractAlignment() { + public int getAlignmentValue(View view, int viewSize) { + return viewSize >> 1; + } + }; + + /** + * Indicates that a view should be aligned with the <em>baselines</em> + * of the other views in its cell group. + * This constant may only be used as an alignment in {@link LayoutParams#rowGroup rowGroups}. + * + * @see View#getBaseline() + */ + public static final Alignment BASELINE = new AbstractAlignment() { + public int getAlignmentValue(View view, int height) { + if (view == null) { + return UNDEFINED; + } + int baseline = view.getBaseline(); + if (baseline == -1) { + return UNDEFINED; + } else { + return baseline; + } + } + + }; + + /** + * Indicates that a view should expanded to fit the boundaries of its cell group. + * This constant may be used in both {@link LayoutParams#rowGroup rowGroups} and + * {@link LayoutParams#columnGroup columnGroups}. + */ + public static final Alignment FILL = new Alignment() { + public int getAlignmentValue(View view, int viewSize) { + return UNDEFINED; + } + + public int getSizeInCell(View view, int viewSize, int cellSize) { + return cellSize; + } + }; +}
\ No newline at end of file diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 0383b5c..732cedc 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -1408,19 +1408,20 @@ public class GridView extends AbsListView { int childLeft; final int childTop = flow ? y : y - h; - switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - childLeft = childrenLeft; - break; - case Gravity.CENTER_HORIZONTAL: - childLeft = childrenLeft + ((mColumnWidth - w) / 2); - break; - case Gravity.RIGHT: - childLeft = childrenLeft + mColumnWidth - w; - break; - default: - childLeft = childrenLeft; - break; + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity,isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + childLeft = childrenLeft; + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = childrenLeft + ((mColumnWidth - w) / 2); + break; + case Gravity.RIGHT: + childLeft = childrenLeft + mColumnWidth - w; + break; + default: + childLeft = childrenLeft; + break; } if (needToMeasure) { diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 1fe6f4b..4b870ec 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -187,6 +187,11 @@ public class ImageView extends View { } @Override + public boolean isLayoutRtl(Drawable dr) { + return (dr == mDrawable) ? isLayoutRtl() : super.isLayoutRtl(dr); + } + + @Override protected boolean onSetAlpha(int alpha) { if (getBackground() == null) { int scale = alpha + (alpha >> 7); @@ -218,15 +223,16 @@ public class ImageView extends View { /** * An optional argument to supply a maximum width for this view. Only valid if - * {@link #setAdjustViewBounds} has been set to true. To set an image to be a maximum of 100 x - * 100 while preserving the original aspect ratio, do the following: 1) set adjustViewBounds to - * true 2) set maxWidth and maxHeight to 100 3) set the height and width layout params to - * WRAP_CONTENT. + * {@link #setAdjustViewBounds(boolean)} has been set to true. To set an image to be a maximum + * of 100 x 100 while preserving the original aspect ratio, do the following: 1) set + * adjustViewBounds to true 2) set maxWidth and maxHeight to 100 3) set the height and width + * layout params to WRAP_CONTENT. * * <p> * Note that this view could be still smaller than 100 x 100 using this approach if the original * image is small. To set an image to a fixed size, specify that size in the layout params and - * then use {@link #setScaleType} to determine how to fit the image within the bounds. + * then use {@link #setScaleType(android.widget.ImageView.ScaleType)} to determine how to fit + * the image within the bounds. * </p> * * @param maxWidth maximum width for this view @@ -240,15 +246,16 @@ public class ImageView extends View { /** * An optional argument to supply a maximum height for this view. Only valid if - * {@link #setAdjustViewBounds} has been set to true. To set an image to be a maximum of 100 x - * 100 while preserving the original aspect ratio, do the following: 1) set adjustViewBounds to - * true 2) set maxWidth and maxHeight to 100 3) set the height and width layout params to - * WRAP_CONTENT. + * {@link #setAdjustViewBounds(boolean)} has been set to true. To set an image to be a + * maximum of 100 x 100 while preserving the original aspect ratio, do the following: 1) set + * adjustViewBounds to true 2) set maxWidth and maxHeight to 100 3) set the height and width + * layout params to WRAP_CONTENT. * * <p> * Note that this view could be still smaller than 100 x 100 using this approach if the original * image is small. To set an image to a fixed size, specify that size in the layout params and - * then use {@link #setScaleType} to determine how to fit the image within the bounds. + * then use {@link #setScaleType(android.widget.ImageView.ScaleType)} to determine how to fit + * the image within the bounds. * </p> * * @param maxHeight maximum height for this view @@ -272,8 +279,8 @@ public class ImageView extends View { * * <p class="note">This does Bitmap reading and decoding on the UI * thread, which can cause a latency hiccup. If that's a concern, - * consider using {@link #setImageDrawable} or - * {@link #setImageBitmap} and + * consider using {@link #setImageDrawable(android.graphics.drawable.Drawable)} or + * {@link #setImageBitmap(android.graphics.Bitmap)} and * {@link android.graphics.BitmapFactory} instead.</p> * * @param resId the resource identifier of the the drawable @@ -297,8 +304,8 @@ public class ImageView extends View { * * <p class="note">This does Bitmap reading and decoding on the UI * thread, which can cause a latency hiccup. If that's a concern, - * consider using {@link #setImageDrawable} or - * {@link #setImageBitmap} and + * consider using {@link #setImageDrawable(android.graphics.drawable.Drawable)} or + * {@link #setImageBitmap(android.graphics.Bitmap)} and * {@link android.graphics.BitmapFactory} instead.</p> * * @param uri The Uri of an image @@ -902,12 +909,12 @@ public class ImageView extends View { /** * <p>Set the offset of the widget's text baseline from the widget's top - * boundary. This value is overridden by the {@link #setBaselineAlignBottom} + * boundary. This value is overridden by the {@link #setBaselineAlignBottom(boolean)} * property.</p> * * @param baseline The baseline to use, or -1 if none is to be provided. * - * @see #setBaseline + * @see #setBaseline(int) * @attr ref android.R.styleable#ImageView_baseline */ public void setBaseline(int baseline) { diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index fd0e53d..0cdbc5b 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -103,21 +103,39 @@ public class LinearLayout extends ViewGroup { @ViewDebug.ExportedProperty(category = "measurement") private int mOrientation; - @ViewDebug.ExportedProperty(category = "measurement", mapping = { - @ViewDebug.IntToString(from = -1, to = "NONE"), - @ViewDebug.IntToString(from = Gravity.NO_GRAVITY, to = "NONE"), - @ViewDebug.IntToString(from = Gravity.TOP, to = "TOP"), - @ViewDebug.IntToString(from = Gravity.BOTTOM, to = "BOTTOM"), - @ViewDebug.IntToString(from = Gravity.LEFT, to = "LEFT"), - @ViewDebug.IntToString(from = Gravity.RIGHT, to = "RIGHT"), - @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL, to = "CENTER_VERTICAL"), - @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL, to = "FILL_VERTICAL"), - @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"), - @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL, to = "FILL_HORIZONTAL"), - @ViewDebug.IntToString(from = Gravity.CENTER, to = "CENTER"), - @ViewDebug.IntToString(from = Gravity.FILL, to = "FILL") + @ViewDebug.ExportedProperty(category = "measurement", flagMapping = { + @ViewDebug.FlagToString(mask = -1, + equals = -1, name = "NONE"), + @ViewDebug.FlagToString(mask = Gravity.NO_GRAVITY, + equals = Gravity.NO_GRAVITY,name = "NONE"), + @ViewDebug.FlagToString(mask = Gravity.TOP, + equals = Gravity.TOP, name = "TOP"), + @ViewDebug.FlagToString(mask = Gravity.BOTTOM, + equals = Gravity.BOTTOM, name = "BOTTOM"), + @ViewDebug.FlagToString(mask = Gravity.LEFT, + equals = Gravity.LEFT, name = "LEFT"), + @ViewDebug.FlagToString(mask = Gravity.RIGHT, + equals = Gravity.RIGHT, name = "RIGHT"), + @ViewDebug.FlagToString(mask = Gravity.START, + equals = Gravity.START, name = "START"), + @ViewDebug.FlagToString(mask = Gravity.END, + equals = Gravity.END, name = "END"), + @ViewDebug.FlagToString(mask = Gravity.CENTER_VERTICAL, + equals = Gravity.CENTER_VERTICAL, name = "CENTER_VERTICAL"), + @ViewDebug.FlagToString(mask = Gravity.FILL_VERTICAL, + equals = Gravity.FILL_VERTICAL, name = "FILL_VERTICAL"), + @ViewDebug.FlagToString(mask = Gravity.CENTER_HORIZONTAL, + equals = Gravity.CENTER_HORIZONTAL, name = "CENTER_HORIZONTAL"), + @ViewDebug.FlagToString(mask = Gravity.FILL_HORIZONTAL, + equals = Gravity.FILL_HORIZONTAL, name = "FILL_HORIZONTAL"), + @ViewDebug.FlagToString(mask = Gravity.CENTER, + equals = Gravity.CENTER, name = "CENTER"), + @ViewDebug.FlagToString(mask = Gravity.FILL, + equals = Gravity.FILL, name = "FILL"), + @ViewDebug.FlagToString(mask = Gravity.RELATIVE_LAYOUT_DIRECTION, + equals = Gravity.RELATIVE_LAYOUT_DIRECTION, name = "RELATIVE") }) - private int mGravity = Gravity.LEFT | Gravity.TOP; + private int mGravity = Gravity.START | Gravity.TOP; @ViewDebug.ExportedProperty(category = "measurement") private int mTotalLength; @@ -201,6 +219,11 @@ public class LinearLayout extends ViewGroup { mShowDividers = showDividers; } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * @return A flag set indicating how dividers should be shown around items. * @see #setShowDividers(int) @@ -230,6 +253,39 @@ public class LinearLayout extends ViewGroup { requestLayout(); } + /** + * Set padding displayed on both ends of dividers. + * + * @param padding Padding value in pixels that will be applied to each end + * + * @see #setShowDividers(int) + * @see #setDividerDrawable(Drawable) + * @see #getDividerPadding() + */ + public void setDividerPadding(int padding) { + mDividerPadding = padding; + } + + /** + * Get the padding size used to inset dividers in pixels + * + * @see #setShowDividers(int) + * @see #setDividerDrawable(Drawable) + * @see #setDividerPadding(int) + */ + public int getDividerPadding() { + return mDividerPadding; + } + + /** + * Get the width of the current divider drawable. + * + * @hide Used internally by framework. + */ + public int getDividerWidth() { + return mDividerWidth; + } + @Override protected void onDraw(Canvas canvas) { if (mDivider == null) { @@ -244,29 +300,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 +318,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 +342,7 @@ public class LinearLayout extends ViewGroup { } } - if (showDividerEnd) { + if (hasDividerBeforeChildAt(count)) { drawVerticalDivider(canvas, left); } } @@ -523,6 +551,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 +599,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 +613,7 @@ public class LinearLayout extends ViewGroup { continue; } - if (firstVisible) { - firstVisible = false; - if (showDividerBeginning) { - mTotalLength += mDividerHeight; - } - } else if (showDividerMiddle) { + if (hasDividerBeforeChildAt(i)) { mTotalLength += mDividerHeight; } @@ -677,11 +710,12 @@ 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; } - if (useLargestChild && heightMode == MeasureSpec.AT_MOST) { + if (useLargestChild && + (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) { mTotalLength = 0; for (int i = 0; i < count; ++i) { @@ -794,6 +828,31 @@ public class LinearLayout extends ViewGroup { } else { alternativeMaxWidth = Math.max(alternativeMaxWidth, weightedMaxWidth); + + + // We have no limit, so make all weighted views as tall as the largest child. + // Children will have already been measured once. + if (useLargestChild && widthMode == MeasureSpec.UNSPECIFIED) { + for (int i = 0; i < count; i++) { + final View child = getVirtualChildAt(i); + + if (child == null || child.getVisibility() == View.GONE) { + continue; + } + + final LinearLayout.LayoutParams lp = + (LinearLayout.LayoutParams) child.getLayoutParams(); + + float childExtra = lp.weight; + if (childExtra > 0) { + child.measure( + MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(largestChildHeight, + MeasureSpec.EXACTLY)); + } + } + } } if (!allFillParent && widthMode != MeasureSpec.EXACTLY) { @@ -881,14 +940,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 +954,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 +1069,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; } @@ -1041,7 +1088,8 @@ public class LinearLayout extends ViewGroup { maxHeight = Math.max(maxHeight, ascent + descent); } - if (useLargestChild && widthMode == MeasureSpec.AT_MOST) { + if (useLargestChild && + (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED)) { mTotalLength = 0; for (int i = 0; i < count; ++i) { @@ -1197,6 +1245,29 @@ public class LinearLayout extends ViewGroup { } } else { alternativeMaxHeight = Math.max(alternativeMaxHeight, weightedMaxHeight); + + // We have no limit, so make all weighted views as wide as the largest child. + // Children will have already been measured once. + if (useLargestChild && widthMode == MeasureSpec.UNSPECIFIED) { + for (int i = 0; i < count; i++) { + final View child = getVirtualChildAt(i); + + if (child == null || child.getVisibility() == View.GONE) { + continue; + } + + final LinearLayout.LayoutParams lp = + (LinearLayout.LayoutParams) child.getLayoutParams(); + + float childExtra = lp.weight; + if (childExtra > 0) { + child.measure( + MeasureSpec.makeMeasureSpec(largestChildWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), + MeasureSpec.EXACTLY)); + } + } + } } if (!allFillParent && heightMode != MeasureSpec.EXACTLY) { @@ -1328,7 +1399,7 @@ public class LinearLayout extends ViewGroup { void layoutVertical() { final int paddingLeft = mPaddingLeft; - int childTop = mPaddingTop; + int childTop; int childLeft; // Where right end of child should go @@ -1341,28 +1412,23 @@ public class LinearLayout extends ViewGroup { final int count = getVirtualChildCount(); final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; - final int minorGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; - - if (majorGravity != Gravity.TOP) { - switch (majorGravity) { - case Gravity.BOTTOM: - // mTotalLength contains the padding already, we add the top - // padding to compensate - childTop = mBottom - mTop + mPaddingTop - mTotalLength; - break; - - case Gravity.CENTER_VERTICAL: - childTop += ((mBottom - mTop) - mTotalLength) / 2; - break; - } - - } - - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - - if ((mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING) { - childTop += mDividerHeight; + final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; + + switch (majorGravity) { + case Gravity.BOTTOM: + // mTotalLength contains the padding already + childTop = mPaddingTop + mBottom - mTop - mTotalLength; + break; + + // mTotalLength contains the padding already + case Gravity.CENTER_VERTICAL: + childTop = mPaddingTop + (mBottom - mTop - mTotalLength) / 2; + break; + + case Gravity.TOP: + default: + childTop = mPaddingTop; + break; } for (int i = 0; i < count; i++) { @@ -1380,12 +1446,8 @@ public class LinearLayout extends ViewGroup { if (gravity < 0) { gravity = minorGravity; } - - switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { - case Gravity.LEFT: - childLeft = paddingLeft + lp.leftMargin; - break; - + final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = paddingLeft + ((childSpace - childWidth) / 2) + lp.leftMargin - lp.rightMargin; @@ -1394,20 +1456,22 @@ public class LinearLayout extends ViewGroup { case Gravity.RIGHT: childLeft = childRight - childWidth - lp.rightMargin; break; + + case Gravity.LEFT: default: - childLeft = paddingLeft; + childLeft = paddingLeft + lp.leftMargin; 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); } } @@ -1422,10 +1486,11 @@ public class LinearLayout extends ViewGroup { * @see #onLayout(boolean, int, int, int, int) */ void layoutHorizontal() { + final boolean isLayoutRtl = isLayoutRtl(); final int paddingTop = mPaddingTop; int childTop; - int childLeft = mPaddingLeft; + int childLeft; // Where bottom of child should go final int height = mBottom - mTop; @@ -1436,7 +1501,7 @@ public class LinearLayout extends ViewGroup { final int count = getVirtualChildCount(); - final int majorGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int majorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; final int minorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final boolean baselineAligned = mBaselineAligned; @@ -1444,32 +1509,37 @@ public class LinearLayout extends ViewGroup { final int[] maxAscent = mMaxAscent; final int[] maxDescent = mMaxDescent; - if (majorGravity != Gravity.LEFT) { - switch (majorGravity) { - case Gravity.RIGHT: - // mTotalLength contains the padding already, we add the left - // padding to compensate - childLeft = mRight - mLeft + mPaddingLeft - mTotalLength; - break; - - case Gravity.CENTER_HORIZONTAL: - childLeft += ((mRight - mLeft) - mTotalLength) / 2; - break; - } + switch (Gravity.getAbsoluteGravity(majorGravity, isLayoutRtl())) { + case Gravity.RIGHT: + // mTotalLength contains the padding already + childLeft = mPaddingLeft + mRight - mLeft - mTotalLength; + break; + + case Gravity.CENTER_HORIZONTAL: + // mTotalLength contains the padding already + childLeft = mPaddingLeft + (mRight - mLeft - mTotalLength) / 2; + break; + + case Gravity.LEFT: + default: + childLeft = mPaddingLeft; + break; } - final boolean showDividerMiddle = - (mShowDividers & SHOW_DIVIDER_MIDDLE) == SHOW_DIVIDER_MIDDLE; - - if ((mShowDividers & SHOW_DIVIDER_BEGINNING) == SHOW_DIVIDER_BEGINNING) { - childLeft += mDividerWidth; + int start = 0; + int dir = 1; + //In case of RTL, start drawing from the last child. + if (isLayoutRtl) { + start = count - 1; + dir = -1; } for (int i = 0; i < count; i++) { - final View child = getVirtualChildAt(i); + int childIndex = start + dir * i; + final View child = getVirtualChildAt(childIndex); if (child == null) { - childLeft += measureNullChild(i); + childLeft += measureNullChild(childIndex); } else if (child.getVisibility() != GONE) { final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); @@ -1523,17 +1593,17 @@ public class LinearLayout extends ViewGroup { break; } + if (hasDividerBeforeChildAt(childIndex)) { + 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); + i += getChildrenSkipCount(child, childIndex); } } } @@ -1578,8 +1648,8 @@ public class LinearLayout extends ViewGroup { @android.view.RemotableViewMethod public void setGravity(int gravity) { if (mGravity != gravity) { - if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { - gravity |= Gravity.LEFT; + if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.START; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { @@ -1593,9 +1663,9 @@ public class LinearLayout extends ViewGroup { @android.view.RemotableViewMethod public void setHorizontalGravity(int horizontalGravity) { - final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK; - if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) { - mGravity = (mGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK) | gravity; + final int gravity = horizontalGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; + if ((mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != gravity) { + mGravity = (mGravity & ~Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravity; requestLayout(); } } @@ -1672,6 +1742,8 @@ public class LinearLayout extends ViewGroup { @ViewDebug.IntToString(from = Gravity.BOTTOM, to = "BOTTOM"), @ViewDebug.IntToString(from = Gravity.LEFT, to = "LEFT"), @ViewDebug.IntToString(from = Gravity.RIGHT, to = "RIGHT"), + @ViewDebug.IntToString(from = Gravity.START, to = "START"), + @ViewDebug.IntToString(from = Gravity.END, to = "END"), @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL, to = "CENTER_VERTICAL"), @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL, to = "FILL_VERTICAL"), @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"), diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index af954c9..e7a9e41 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -251,7 +251,7 @@ public class ListView extends AbsListView { */ public void addHeaderView(View v, Object data, boolean isSelectable) { - if (mAdapter != null) { + if (mAdapter != null && ! (mAdapter instanceof HeaderViewListAdapter)) { throw new IllegalStateException( "Cannot add header view to list -- setAdapter has already been called."); } @@ -261,6 +261,12 @@ public class ListView extends AbsListView { info.data = data; info.isSelectable = isSelectable; mHeaderViewInfos.add(info); + + // in the case of re-adding a header view, or adding one later on, + // we need to notify the observer + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } } /** @@ -294,7 +300,9 @@ public class ListView extends AbsListView { if (mHeaderViewInfos.size() > 0) { boolean result = false; if (((HeaderViewListAdapter) mAdapter).removeHeader(v)) { - mDataSetObserver.onChanged(); + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } result = true; } removeFixedViewInfo(v, mHeaderViewInfos); @@ -328,6 +336,12 @@ public class ListView extends AbsListView { * @param isSelectable true if the footer view can be selected */ public void addFooterView(View v, Object data, boolean isSelectable) { + + // NOTE: do not enforce the adapter being null here, since unlike in + // addHeaderView, it was never enforced here, and so existing apps are + // relying on being able to add a footer and then calling setAdapter to + // force creation of the HeaderViewListAdapter wrapper + FixedViewInfo info = new FixedViewInfo(); info.view = v; info.data = data; @@ -371,7 +385,9 @@ public class ListView extends AbsListView { if (mFooterViewInfos.size() > 0) { boolean result = false; if (((HeaderViewListAdapter) mAdapter).removeFooter(v)) { - mDataSetObserver.onChanged(); + if (mDataSetObserver != null) { + mDataSetObserver.onChanged(); + } result = true; } removeFixedViewInfo(v, mFooterViewInfos); @@ -1552,7 +1568,7 @@ public class ListView extends AbsListView { // take focus back to us temporarily to avoid the eventual // call to clear focus when removing the focused child below - // from messing things up when ViewRoot assigns focus back + // from messing things up when ViewAncestor assigns focus back // to someone else final View focusedChild = getFocusedChild(); if (focusedChild != null) { @@ -1982,36 +1998,28 @@ public class ListView extends AbsListView { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - boolean populated = super.dispatchPopulateAccessibilityEvent(event); + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); // If the item count is less than 15 then subtract disabled items from the count and // position. Otherwise ignore disabled items. - if (!populated) { - int itemCount = 0; - int currentItemIndex = getSelectedItemPosition(); - - ListAdapter adapter = getAdapter(); - if (adapter != null) { - final int count = adapter.getCount(); - if (count < 15) { - for (int i = 0; i < count; i++) { - if (adapter.isEnabled(i)) { - itemCount++; - } else if (i <= currentItemIndex) { - currentItemIndex--; - } - } - } else { - itemCount = count; + int itemCount = 0; + int currentItemIndex = getSelectedItemPosition(); + + ListAdapter adapter = getAdapter(); + if (adapter != null) { + final int count = adapter.getCount(); + for (int i = 0; i < count; i++) { + if (adapter.isEnabled(i)) { + itemCount++; + } else if (i <= currentItemIndex) { + currentItemIndex--; } } - - event.setItemCount(itemCount); - event.setCurrentItemIndex(currentItemIndex); } - return populated; + event.setItemCount(itemCount); + event.setCurrentItemIndex(currentItemIndex); } /** diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java index 02c1ec7..134e4c4 100644 --- a/core/java/android/widget/MultiAutoCompleteTextView.java +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -30,7 +30,7 @@ import android.widget.MultiAutoCompleteTextView.Tokenizer; * can show completion suggestions for the substring of the text where * the user is typing instead of necessarily for the entire thing. * <p> - * You must must provide a {@link Tokenizer} to distinguish the + * You must provide a {@link Tokenizer} to distinguish the * various substrings. * * <p>The following code snippet shows how to create a text view which suggests @@ -41,7 +41,7 @@ import android.widget.MultiAutoCompleteTextView.Tokenizer; * protected void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); * setContentView(R.layout.autocomplete_7); - * + * * ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, * android.R.layout.simple_dropdown_item_1line, COUNTRIES); * MultiAutoCompleteTextView textView = (MultiAutoCompleteTextView) findViewById(R.id.edit); @@ -132,7 +132,7 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { * Instead of validating the entire text, this subclass method validates * each token of the text individually. Empty tokens are removed. */ - @Override + @Override public void performValidation() { Validator v = getValidator(); diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index a5b7281..563fc26 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -392,11 +392,11 @@ public class PopupWindow { mContentView = contentView; - if (mContext == null) { + if (mContext == null && mContentView != null) { mContext = mContentView.getContext(); } - if (mWindowManager == null) { + if (mWindowManager == null && mContentView != null) { mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); } } @@ -939,7 +939,9 @@ public class PopupWindow { * @param p the layout parameters of the popup's content view */ private void invokePopup(WindowManager.LayoutParams p) { - p.packageName = mContext.getPackageName(); + if (mContext != null) { + p.packageName = mContext.getPackageName(); + } mWindowManager.addView(mPopupView, p); } diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index 6b676b4..ed9114a 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -16,6 +16,8 @@ package android.widget; +import com.android.internal.R; + import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -41,6 +43,8 @@ import android.view.Gravity; import android.view.RemotableViewMethod; import android.view.View; import android.view.ViewDebug; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -49,8 +53,6 @@ import android.view.animation.LinearInterpolator; import android.view.animation.Transformation; import android.widget.RemoteViews.RemoteView; -import com.android.internal.R; - /** * <p> @@ -187,6 +189,7 @@ import com.android.internal.R; public class ProgressBar extends View { private static final int MAX_LEVEL = 10000; private static final int ANIMATION_RESOLUTION = 200; + private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200; int mMinWidth; int mMaxWidth; @@ -218,6 +221,8 @@ public class ProgressBar extends View { private int mAnimationResolution; + private AccessibilityEventSender mAccessibilityEventSender; + /** * Create a new progress bar with range 0...100 and initial progress of 0. * @param context the application environment @@ -604,8 +609,11 @@ public class ProgressBar extends View { onProgressRefresh(scale, fromUser); } } - - void onProgressRefresh(float scale, boolean fromUser) { + + void onProgressRefresh(float scale, boolean fromUser) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + scheduleAccessibilityEventSender(); + } } private synchronized void refreshProgress(int id, int progress, boolean fromUser) { @@ -908,6 +916,12 @@ public class ProgressBar extends View { } @Override + public boolean isLayoutRtl(Drawable who) { + return (who == mProgressDrawable || who == mIndeterminateDrawable) ? + isLayoutRtl() : super.isLayoutRtl(who); + } + + @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { updateDrawableBounds(w, h); } @@ -1069,8 +1083,46 @@ public class ProgressBar extends View { if (mIndeterminate) { stopAnimation(); } + if(mRefreshProgressRunnable != null) { + removeCallbacks(mRefreshProgressRunnable); + } + if (mAccessibilityEventSender != null) { + removeCallbacks(mAccessibilityEventSender); + } // This should come after stopAnimation(), otherwise an invalidate message remains in the // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation super.onDetachedFromWindow(); } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setItemCount(mMax); + event.setCurrentItemIndex(mProgress); + } + + /** + * Schedule a command for sending an accessibility event. + * </br> + * Note: A command is used to ensure that accessibility events + * are sent at most one in a given time frame to save + * system resources while the progress changes quickly. + */ + private void scheduleAccessibilityEventSender() { + if (mAccessibilityEventSender == null) { + mAccessibilityEventSender = new AccessibilityEventSender(); + } else { + removeCallbacks(mAccessibilityEventSender); + } + postDelayed(mAccessibilityEventSender, TIMEOUT_SEND_ACCESSIBILITY_EVENT); + } + + /** + * Command for sending an accessibility event. + */ + private class AccessibilityEventSender implements Runnable { + public void run() { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + } } diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index a47359f..a4771d5 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -18,17 +18,23 @@ package android.widget; import com.android.internal.R; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.SortedSet; +import java.util.TreeSet; + import android.content.Context; -import android.content.res.TypedArray; import android.content.res.Resources; +import android.content.res.TypedArray; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.SparseArray; -import android.util.Poolable; import android.util.Pool; -import android.util.Pools; +import android.util.Poolable; import android.util.PoolableManager; -import static android.util.Log.d; +import android.util.Pools; +import android.util.SparseArray; import android.view.Gravity; import android.view.View; import android.view.ViewDebug; @@ -36,12 +42,7 @@ import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.RemoteViews.RemoteView; -import java.util.Comparator; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.LinkedList; -import java.util.HashSet; -import java.util.ArrayList; +import static android.util.Log.d; /** * A Layout where the positions of the children can be described in relation to each other or to the @@ -186,6 +187,11 @@ public class RelativeLayout extends ViewGroup { a.recycle(); } + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + /** * Defines which View is ignored when the gravity is applied. This setting has no * effect if the gravity is <code>Gravity.LEFT | Gravity.TOP</code>. @@ -216,8 +222,8 @@ public class RelativeLayout extends ViewGroup { @android.view.RemotableViewMethod public void setGravity(int gravity) { if (mGravity != gravity) { - if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { - gravity |= Gravity.LEFT; + if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.START; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { @@ -231,9 +237,9 @@ public class RelativeLayout extends ViewGroup { @android.view.RemotableViewMethod public void setHorizontalGravity(int horizontalGravity) { - final int gravity = horizontalGravity & Gravity.HORIZONTAL_GRAVITY_MASK; - if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != gravity) { - mGravity = (mGravity & ~Gravity.HORIZONTAL_GRAVITY_MASK) | gravity; + final int gravity = horizontalGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; + if ((mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != gravity) { + mGravity = (mGravity & ~Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravity; requestLayout(); } } @@ -334,7 +340,7 @@ public class RelativeLayout extends ViewGroup { mHasBaselineAlignedChild = false; View ignore = null; - int gravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; final boolean horizontalGravity = gravity != Gravity.LEFT && gravity != 0; gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0; @@ -489,7 +495,8 @@ public class RelativeLayout extends ViewGroup { height - mPaddingBottom); final Rect contentBounds = mContentBounds; - Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds); + Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds, + isLayoutRtl()); final int horizontalOffset = contentBounds.left - left; final int verticalOffset = contentBounds.top - top; @@ -1434,6 +1441,7 @@ public class RelativeLayout extends ViewGroup { ); private Node mNext; + private boolean mIsPooled; public void setNextPoolable(Node element) { mNext = element; @@ -1443,6 +1451,14 @@ public class RelativeLayout extends ViewGroup { return mNext; } + public boolean isPooled() { + return mIsPooled; + } + + public void setPooled(boolean isPooled) { + mIsPooled = isPooled; + } + static Node acquire(View view) { final Node node = sPool.acquire(); node.view = view; diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index c854fac..9cf2718 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -125,7 +125,7 @@ public class RemoteViews implements Parcelable, Filter { * SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!! */ private abstract static class Action implements Parcelable { - public abstract void apply(View root) throws ActionException; + public abstract void apply(View root, ViewGroup rootParent) throws ActionException; public int describeContents() { return 0; @@ -183,7 +183,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View view = root.findViewById(viewId); if (!(view instanceof AdapterView<?>)) return; @@ -214,7 +214,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -295,7 +295,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -360,6 +360,60 @@ public class RemoteViews implements Parcelable, Filter { public final static int TAG = 8; } + private class SetRemoteViewsAdapterIntent extends Action { + public SetRemoteViewsAdapterIntent(int id, Intent intent) { + this.viewId = id; + this.intent = intent; + } + + public SetRemoteViewsAdapterIntent(Parcel parcel) { + viewId = parcel.readInt(); + intent = Intent.CREATOR.createFromParcel(parcel); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + intent.writeToParcel(dest, flags); + } + + @Override + public void apply(View root, ViewGroup rootParent) { + final View target = root.findViewById(viewId); + if (target == null) return; + + // Ensure that we are applying to an AppWidget root + if (!(rootParent instanceof AppWidgetHostView)) { + Log.e("RemoteViews", "SetRemoteViewsAdapterIntent action can only be used for " + + "AppWidgets (root id: " + viewId + ")"); + return; + } + // Ensure that we are calling setRemoteAdapter on an AdapterView that supports it + if (!(target instanceof AbsListView) && !(target instanceof AdapterViewAnimator)) { + Log.e("RemoteViews", "Cannot setRemoteViewsAdapter on a view which is not " + + "an AbsListView or AdapterViewAnimator (id: " + viewId + ")"); + return; + } + + // Embed the AppWidget Id for use in RemoteViewsAdapter when connecting to the intent + // RemoteViewsService + AppWidgetHostView host = (AppWidgetHostView) rootParent; + intent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, host.getAppWidgetId()); + if (target instanceof AbsListView) { + AbsListView v = (AbsListView) target; + v.setRemoteViewsAdapter(intent); + } else if (target instanceof AdapterViewAnimator) { + AdapterViewAnimator v = (AdapterViewAnimator) target; + v.setRemoteViewsAdapter(intent); + } + } + + int viewId; + Intent intent; + + public final static int TAG = 10; + } + /** * Equivalent to calling * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)} @@ -383,7 +437,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -479,7 +533,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View target = root.findViewById(viewId); if (target == null) return; @@ -539,7 +593,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View view = root.findViewById(viewId); if (view == null) return; @@ -755,7 +809,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final View view = root.findViewById(viewId); if (view == null) return; @@ -850,7 +904,7 @@ public class RemoteViews implements Parcelable, Filter { } @Override - public void apply(View root) { + public void apply(View root, ViewGroup rootParent) { final Context context = root.getContext(); final ViewGroup target = (ViewGroup) root.findViewById(viewId); if (target == null) return; @@ -952,6 +1006,9 @@ public class RemoteViews implements Parcelable, Filter { case SetOnClickFillInIntent.TAG: mActions.add(new SetOnClickFillInIntent(parcel)); break; + case SetRemoteViewsAdapterIntent.TAG: + mActions.add(new SetRemoteViewsAdapterIntent(parcel)); + break; default: throw new ActionException("Tag " + tag + " not found"); } @@ -1287,16 +1344,29 @@ public class RemoteViews implements Parcelable, Filter { /** * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}. * - * @param appWidgetId The id of the app widget which contains the specified view + * @param appWidgetId The id of the app widget which contains the specified view. (This + * parameter is ignored in this deprecated method) * @param viewId The id of the view whose text should change * @param intent The intent of the service which will be * providing data to the RemoteViewsAdapter + * @deprecated This method has been deprecated. See + * {@link android.widget.RemoteViews#setRemoteAdapter(int, Intent)} */ + @Deprecated public void setRemoteAdapter(int appWidgetId, int viewId, Intent intent) { - // Embed the AppWidget Id for use in RemoteViewsAdapter when connecting to the intent - // RemoteViewsService - intent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, appWidgetId); - setIntent(viewId, "setRemoteViewsAdapter", intent); + setRemoteAdapter(viewId, intent); + } + + /** + * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}. + * Can only be used for App Widgets. + * + * @param viewId The id of the view whose text should change + * @param intent The intent of the service which will be + * providing data to the RemoteViewsAdapter + */ + public void setRemoteAdapter(int viewId, Intent intent) { + addAction(new SetRemoteViewsAdapterIntent(viewId, intent)); } /** @@ -1499,7 +1569,7 @@ public class RemoteViews implements Parcelable, Filter { result = inflater.inflate(mLayoutId, parent, false); - performApply(result); + performApply(result, parent); return result; } @@ -1514,15 +1584,15 @@ public class RemoteViews implements Parcelable, Filter { */ public void reapply(Context context, View v) { prepareContext(context); - performApply(v); + performApply(v, (ViewGroup) v.getParent()); } - private void performApply(View v) { + private void performApply(View v, ViewGroup parent) { if (mActions != null) { final int count = mActions.size(); for (int i = 0; i < count; i++) { Action a = mActions.get(i); - a.apply(v); + a.apply(v, parent); } } } diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java index 1c0a2bb..40b0a9c 100644 --- a/core/java/android/widget/RemoteViewsAdapter.java +++ b/core/java/android/widget/RemoteViewsAdapter.java @@ -29,6 +29,7 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.RemoteException; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -156,13 +157,16 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback // create in response to this bind factory.onDataSetChanged(); } - } catch (Exception e) { + } catch (RemoteException e) { Log.e(TAG, "Error notifying factory of data set changed in " + "onServiceConnected(): " + e.getMessage()); // Return early to prevent anything further from being notified // (effectively nothing has changed) return; + } catch (RuntimeException e) { + Log.e(TAG, "Error notifying factory of data set changed in " + + "onServiceConnected(): " + e.getMessage()); } // Request meta data so that we have up to date data when calling back to @@ -777,7 +781,9 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback tmpMetaData.count = count; tmpMetaData.setLoadingViewTemplates(loadingView, firstView); } - } catch (Exception e) { + } catch(RemoteException e) { + processException("updateMetaData", e); + } catch(RuntimeException e) { processException("updateMetaData", e); } } @@ -792,12 +798,15 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback try { remoteViews = factory.getViewAt(position); itemId = factory.getItemId(position); - } catch (Exception e) { + } catch (RemoteException e) { Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage()); // Return early to prevent additional work in re-centering the view cache, and // swapping from the loading view return; + } catch (RuntimeException e) { + Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage()); + return; } if (remoteViews == null) { @@ -971,18 +980,20 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback return getCount() <= 0; } - private void onNotifyDataSetChanged() { // Complete the actual notifyDataSetChanged() call initiated earlier IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory(); try { factory.onDataSetChanged(); - } catch (Exception e) { + } catch (RemoteException e) { Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); // Return early to prevent from further being notified (since nothing has // changed) return; + } catch (RuntimeException e) { + Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage()); + return; } // Flush the cache so that we can reload new items from the service diff --git a/core/java/android/widget/RemoteViewsService.java b/core/java/android/widget/RemoteViewsService.java index e0b08d4..7ba4777 100644 --- a/core/java/android/widget/RemoteViewsService.java +++ b/core/java/android/widget/RemoteViewsService.java @@ -138,34 +138,87 @@ public abstract class RemoteViewsService extends Service { return mIsCreated; } public synchronized void onDataSetChanged() { - mFactory.onDataSetChanged(); + try { + mFactory.onDataSetChanged(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } } public synchronized int getCount() { - return mFactory.getCount(); + int count = 0; + try { + count = mFactory.getCount(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return count; } public synchronized RemoteViews getViewAt(int position) { - RemoteViews rv = mFactory.getViewAt(position); - rv.setIsWidgetCollectionChild(true); + RemoteViews rv = null; + try { + rv = mFactory.getViewAt(position); + if (rv != null) { + rv.setIsWidgetCollectionChild(true); + } + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } return rv; } public synchronized RemoteViews getLoadingView() { - return mFactory.getLoadingView(); + RemoteViews rv = null; + try { + rv = mFactory.getLoadingView(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return rv; } public synchronized int getViewTypeCount() { - return mFactory.getViewTypeCount(); + int count = 0; + try { + count = mFactory.getViewTypeCount(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return count; } public synchronized long getItemId(int position) { - return mFactory.getItemId(position); + long id = 0; + try { + id = mFactory.getItemId(position); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return id; } public synchronized boolean hasStableIds() { - return mFactory.hasStableIds(); + boolean hasStableIds = false; + try { + hasStableIds = mFactory.hasStableIds(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } + return hasStableIds; } public void onDestroy(Intent intent) { synchronized (sLock) { Intent.FilterComparison fc = new Intent.FilterComparison(intent); if (RemoteViewsService.sRemoteViewFactories.containsKey(fc)) { RemoteViewsFactory factory = RemoteViewsService.sRemoteViewFactories.get(fc); - factory.onDestroy(); + try { + factory.onDestroy(); + } catch (Exception ex) { + Thread t = Thread.currentThread(); + Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex); + } RemoteViewsService.sRemoteViewFactories.remove(fc); } } diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index ade3a0a..27edb88 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -162,6 +162,11 @@ public class ScrollView extends FrameLayout { } @Override + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override protected float getTopFadingEdgeStrength() { if (getChildCount() == 0) { return 0.0f; diff --git a/core/java/android/widget/SearchView.java b/core/java/android/widget/SearchView.java index 9933d68..586ece8 100644 --- a/core/java/android/widget/SearchView.java +++ b/core/java/android/widget/SearchView.java @@ -93,6 +93,7 @@ public class SearchView extends LinearLayout { private boolean mClearingFocus; private int mMaxWidth; private boolean mVoiceButtonEnabled; + private CharSequence mUserQuery; private SearchableInfo mSearchable; private Bundle mAppSearchData; @@ -372,6 +373,7 @@ public class SearchView extends LinearLayout { mQueryTextView.setText(query); if (query != null) { mQueryTextView.setSelection(query.length()); + mUserQuery = query; } // If the query is not empty and submit is requested, submit the query @@ -885,6 +887,7 @@ public class SearchView extends LinearLayout { private void onTextChanged(CharSequence newText) { CharSequence text = mQueryTextView.getText(); + mUserQuery = text; boolean hasText = !TextUtils.isEmpty(text); if (isSubmitButtonEnabled()) { updateSubmitButton(hasText); @@ -1124,7 +1127,7 @@ public class SearchView extends LinearLayout { if (data != null) { intent.setData(data); } - intent.putExtra(SearchManager.USER_QUERY, query); + intent.putExtra(SearchManager.USER_QUERY, mUserQuery); if (query != null) { intent.putExtra(SearchManager.QUERY, query); } diff --git a/core/java/android/widget/SimpleCursorAdapter.java b/core/java/android/widget/SimpleCursorAdapter.java index 3d2a252..c5c6c69 100644 --- a/core/java/android/widget/SimpleCursorAdapter.java +++ b/core/java/android/widget/SimpleCursorAdapter.java @@ -338,6 +338,12 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { @Override public Cursor swapCursor(Cursor c) { + // super.swapCursor() will notify observers before we have + // a valid mapping, make sure we have a mapping before this + // happens + if (mFrom == null) { + findColumns(mOriginalFrom); + } Cursor res = super.swapCursor(c); // rescan columns in case cursor layout is different findColumns(mOriginalFrom); @@ -358,7 +364,13 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { public void changeCursorAndColumns(Cursor c, String[] from, int[] to) { mOriginalFrom = from; mTo = to; - super.changeCursor(c); + // super.changeCursor() will notify observers before we have + // a valid mapping, make sure we have a mapping before this + // happens + if (mFrom == null) { + findColumns(mOriginalFrom); + } + super.changeCursor(c); findColumns(mOriginalFrom); } diff --git a/core/java/android/widget/Space.java b/core/java/android/widget/Space.java new file mode 100644 index 0000000..d98b937 --- /dev/null +++ b/core/java/android/widget/Space.java @@ -0,0 +1,75 @@ +/* + * 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.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * Space is a lightweight View subclass that may be used to create gaps between components + * in general purpose layouts. + */ +public final class Space extends View { + /** + * {@inheritDoc} + */ + public Space(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * {@inheritDoc} + */ + public Space(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * {@inheritDoc} + */ + public Space(Context context) { + super(context); + } + + /** + * Draw nothing. + * + * @param canvas an unused parameter. + */ + @Override + public void draw(Canvas canvas) { + } + + /** + * {@inheritDoc} + */ + @Override + public ViewGroup.LayoutParams getLayoutParams() { + return super.getLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setLayoutParams(ViewGroup.LayoutParams params) { + super.setLayoutParams(params); + } +} diff --git a/core/java/android/widget/StackView.java b/core/java/android/widget/StackView.java index 21c61bd..c4ba7c8 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; @@ -115,7 +116,7 @@ public class StackView extends AdapterViewAnimator { private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000; - private static long MIN_TIME_BETWEEN_SCROLLS = 100; + private static final long MIN_TIME_BETWEEN_SCROLLS = 100; /** * These variables are all related to the current state of touch interaction @@ -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(); } @@ -198,8 +213,7 @@ public class StackView extends AdapterViewAnimator { * Animate the views between different relative indexes within the {@link AdapterViewAnimator} */ void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) { - ObjectAnimator alphaOa = null; - ObjectAnimator oldAlphaOa = null; + ObjectAnimator alphaOa; if (!animate) { ((StackFrame) view).cancelSliderAnimator(); @@ -357,7 +371,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 +426,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 +443,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()); } @@ -1261,13 +1275,11 @@ public class StackView extends AdapterViewAnimator { boolean firstPass = true; parentRect.set(0, 0, 0, 0); - int depth = 0; while (p.getParent() != null && p.getParent() instanceof View && !parentRect.contains(globalInvalidateRect)) { if (!firstPass) { globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop() - p.getScrollY()); - depth++; } firstPass = false; p = (View) p.getParent(); @@ -1355,16 +1367,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/Switch.java b/core/java/android/widget/Switch.java index cd4b732..5c6a26f 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -16,8 +16,6 @@ package android.widget; -import com.android.internal.R; - import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -37,6 +35,8 @@ import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.ViewConfiguration; +import com.android.internal.R; + /** * A Switch is a two-state toggle switch widget that can select between two * options. The user may drag the "thumb" back and forth to choose the selected option, @@ -84,6 +84,7 @@ public class Switch extends CompoundButton { private Layout mOnLayout; private Layout mOffLayout; + @SuppressWarnings("hiding") private final Rect mTempRect = new Rect(); private static final int[] CHECKED_STATE_SET = { diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java index 6f76dd0..1fe1f79 100644 --- a/core/java/android/widget/TabWidget.java +++ b/core/java/android/widget/TabWidget.java @@ -427,12 +427,19 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - event.setItemCount(getTabCount()); - event.setCurrentItemIndex(mSelectedTab); + onPopulateAccessibilityEvent(event); + // Dispatch only to the selected tab. if (mSelectedTab != -1) { - getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event); + return getChildTabViewAt(mSelectedTab).dispatchPopulateAccessibilityEvent(event); } - return true; + return false; + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setItemCount(getTabCount()); + event.setCurrentItemIndex(mSelectedTab); } /** diff --git a/core/java/android/widget/TableRow.java b/core/java/android/widget/TableRow.java index b612004..5f20c85 100644 --- a/core/java/android/widget/TableRow.java +++ b/core/java/android/widget/TableRow.java @@ -224,7 +224,8 @@ public class TableRow extends LinearLayout { final int childWidth = child.getMeasuredWidth(); lp.mOffset[LayoutParams.LOCATION_NEXT] = columnWidth - childWidth; - switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: // don't offset on X axis break; diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 435cf4e..9a5977a 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -16,11 +16,6 @@ package android.widget; -import com.android.internal.util.FastMath; -import com.android.internal.widget.EditableInputConnection; - -import org.xmlpull.v1.XmlPullParserException; - import android.R; import android.content.ClipData; import android.content.ClipData.Item; @@ -60,6 +55,7 @@ import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; import android.text.SpannableString; +import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.SpannedString; import android.text.StaticLayout; @@ -80,12 +76,17 @@ import android.text.method.SingleLineTransformationMethod; import android.text.method.TextKeyListener; import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; +import android.text.method.WordIterator; import android.text.style.ClickableSpan; import android.text.style.ParagraphStyle; +import android.text.style.SuggestionSpan; +import android.text.style.TextAppearanceSpan; import android.text.style.URLSpan; +import android.text.style.UnderlineSpan; import android.text.style.UpdateAppearance; import android.text.util.Linkify; import android.util.AttributeSet; +import android.util.DisplayMetrics; import android.util.FloatMath; import android.util.Log; import android.util.TypedValue; @@ -102,16 +103,17 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewAncestor; import android.view.ViewConfiguration; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewParent; -import android.view.ViewRoot; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AnimationUtils; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; @@ -123,8 +125,14 @@ import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.RemoteViews.RemoteView; +import com.android.internal.util.FastMath; +import com.android.internal.widget.EditableInputConnection; + +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; import java.lang.ref.WeakReference; +import java.text.BreakIterator; import java.util.ArrayList; /** @@ -309,6 +317,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mTextEditPasteWindowLayout, mTextEditSidePasteWindowLayout; private int mTextEditNoPasteWindowLayout, mTextEditSideNoPasteWindowLayout; + private int mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout; + private int mTextEditSuggestionItemLayout; + private SuggestionsPopupWindow mSuggestionsPopupWindow; + private SuggestionRangeSpan mSuggestionRangeSpan; + private boolean mSuggestionsEnabled = true; + private int mCursorDrawableRes; private final Drawable[] mCursorDrawable = new Drawable[2]; private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2 @@ -317,13 +331,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private Drawable mSelectHandleRight; private Drawable mSelectHandleCenter; - private int mLastDownPositionX, mLastDownPositionY; + private float mLastDownPositionX, mLastDownPositionY; private Callback mCustomSelectionActionModeCallback; private final int mSquaredTouchSlopDistance; // Set when this TextView gained focus with some text selected. Will start selection mode. private boolean mCreatedWithASelection = false; + private WordIterator mWordIterator; + /* * Kick-start the font cache for the zygote process (to pay the cost of * initializing freetype for our default font only once). @@ -777,9 +793,25 @@ 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; + + case com.android.internal.R.styleable.TextView_suggestionsEnabled: + mSuggestionsEnabled = a.getBoolean(attr, true); + break; } } a.recycle(); @@ -2062,8 +2094,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @attr ref android.R.styleable#TextView_gravity */ public void setGravity(int gravity) { - if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) { - gravity |= Gravity.LEFT; + if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) { + gravity |= Gravity.START; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) { gravity |= Gravity.TOP; @@ -2071,8 +2103,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean newLayout = false; - if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != - (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) { + if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != + (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) { newLayout = true; } @@ -2534,6 +2566,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener sp.removeSpan(cw); } + // hideControllers would do it, but it gets called after this method on rotation + sp.removeSpan(mSuggestionRangeSpan); + ss.text = sp; } else { ss.text = mText.toString(); @@ -2872,8 +2907,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener setText(mCharWrapper, mBufferType, false, oldlen); } - private static class CharWrapper - implements CharSequence, GetChars, GraphicsOperations { + private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations { private char[] mChars; private int mStart, mLength; @@ -2949,6 +2983,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener advancesIndex); } + public float getTextRunAdvances(int start, int end, int contextStart, + int contextEnd, int flags, float[] advances, int advancesIndex, + Paint p, int reserved) { + int count = end - start; + int contextCount = contextEnd - contextStart; + return p.getTextRunAdvances(mChars, start + mStart, count, + contextStart + mStart, contextCount, flags, advances, + advancesIndex, reserved); + } + public int getTextRunCursor(int contextStart, int contextEnd, int flags, int offset, int cursorOpt, Paint p) { int contextCount = contextEnd - contextStart; @@ -3337,13 +3381,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Handler h = getHandler(); if (h != null) { long eventTime = SystemClock.uptimeMillis(); - h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE | KeyEvent.FLAG_EDITOR_ACTION))); - h.sendMessage(h.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + h.sendMessage(h.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, new KeyEvent(SystemClock.uptimeMillis(), eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, @@ -3977,13 +4021,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(); @@ -4109,6 +4146,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override + public boolean isLayoutRtl(Drawable who) { + if (who == null) return false; + if (mDrawables != null) { + final Drawables drawables = mDrawables; + if (who == drawables.mDrawableLeft || who == drawables.mDrawableRight || + who == drawables.mDrawableTop || who == drawables.mDrawableBottom) { + return isLayoutRtl(); + } + } + return super.isLayoutRtl(who); + } + + @Override protected boolean onSetAlpha(int alpha) { // Alpha is supported if and only if the drawing can be done in one pass. // TODO text with spans with a background color currently do not respect this alpha. @@ -4210,6 +4260,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 +4337,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) { @@ -4347,9 +4397,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText); } + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl()); if (mEllipsize == TextUtils.TruncateAt.MARQUEE) { if (!mSingleLine && getLineCount() == 1 && canMarquee() && - (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) { + (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) { canvas.translate(mLayout.getLineRight(0) - (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight()), 0.0f); } @@ -4549,15 +4600,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,7 +5278,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 + // intentionally empty } /** @@ -5423,6 +5465,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * of edit operations through a call to link {@link #beginBatchEdit()}. */ public void onBeginBatchEdit() { + // intentionally empty } /** @@ -5430,6 +5473,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * of edit operations through a call to link {@link #endBatchEdit}. */ public void onEndBatchEdit() { + // intentionally empty } /** @@ -5502,7 +5546,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } Layout.Alignment alignment; - switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: alignment = Layout.Alignment.ALIGN_CENTER; break; @@ -6276,15 +6321,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; } } @@ -6757,25 +6802,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** - * This method is called when the text is changed, in case any - * subclasses would like to know. + * This method is called when the text is changed, in case any subclasses + * would like to know. * - * @param text The text the TextView is displaying. - * @param start The offset of the start of the range of the text - * that was modified. - * @param before The offset of the former end of the range of the - * text that was modified. If text was simply inserted, - * this will be the same as <code>start</code>. - * If text was replaced with new text or deleted, the - * length of the old text was <code>before-start</code>. - * @param after The offset of the end of the range of the text - * that was modified. If text was simply deleted, - * this will be the same as <code>start</code>. - * If text was replaced with new text or inserted, - * the length of the new text is <code>after-start</code>. + * Within <code>text</code>, the <code>lengthAfter</code> characters + * beginning at <code>start</code> have just replaced old text that had + * length <code>lengthBefore</code>. It is an error to attempt to make + * changes to <code>text</code> from this callback. + * + * @param text The text the TextView is displaying + * @param start The offset of the start of the range of the text that was + * modified + * @param lengthBefore The length of the former text that has been replaced + * @param lengthAfter The length of the replacement modified text */ - protected void onTextChanged(CharSequence text, - int start, int before, int after) { + protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { + // intentionally empty } /** @@ -6786,6 +6828,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 } /** @@ -7132,7 +7175,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } // The DecorView does not have focus when the 'Done' ExtractEditText button is - // pressed. Since it is the ViewRoot's mView, it requests focus before + // pressed. Since it is the ViewAncestor's mView, it requests focus before // ExtractEditText clears focus, which gives focus to the ExtractEditText. // This special case ensure that we keep current selection in that case. // It would be better to know why the DecorView does not have focus at that time. @@ -7201,14 +7244,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() { @@ -7248,11 +7283,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mInputContentType.enterDown = false; } hideControllers(); + removeAllSuggestionSpans(); } startStopMarquee(hasWindowFocus); } + private void removeAllSuggestionSpans() { + if (mText instanceof Editable) { + Editable editable = ((Editable) mText); + SuggestionSpan[] spans = editable.getSpans(0, mText.length(), SuggestionSpan.class); + final int length = spans.length; + for (int i = 0; i < length; i++) { + editable.removeSpan(spans[i]); + } + } + } + @Override protected void onVisibilityChanged(View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); @@ -7299,8 +7346,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (action == MotionEvent.ACTION_DOWN) { - mLastDownPositionX = (int) event.getX(); - mLastDownPositionY = (int) event.getY(); + mLastDownPositionX = event.getX(); + mLastDownPositionY = event.getY(); // Reset this state; it will be re-set if super.onTouchEvent // causes focus to move to the view. @@ -7327,9 +7374,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); } @@ -7346,27 +7390,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(); } } } @@ -7544,7 +7582,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return 0.0f; } } else if (getLineCount() == 1) { - switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: return 0.0f; case Gravity.RIGHT: @@ -7567,7 +7606,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final Marquee marquee = mMarquee; return (marquee.mMaxFadeScroll - marquee.mScroll) / getHorizontalFadingEdgeLength(); } else if (getLineCount() == 1) { - switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, isLayoutRtl()); + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() - getCompoundPaddingRight(); @@ -7608,7 +7648,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener protected int computeVerticalScrollExtent() { return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom(); } - + + @Override + public void findViewsWithText(ArrayList<View> outViews, CharSequence text) { + CharSequence thisText = getText(); + if (TextUtils.isEmpty(thisText)) { + return; + } + if (thisText.toString().toLowerCase().contains(text)) { + outViews.add(this); + } + } + public enum BufferType { NORMAL, SPANNABLE, EDITABLE, } @@ -7743,88 +7794,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener hasPrimaryClip()); } - private boolean isWordCharacter(int c, int type) { - return (c == '\'' || c == '"' || - type == Character.UPPERCASE_LETTER || - type == Character.LOWERCASE_LETTER || - type == Character.TITLECASE_LETTER || - type == Character.MODIFIER_LETTER || - type == Character.OTHER_LETTER || // Should handle asian characters - type == Character.DECIMAL_DIGIT_NUMBER); - } - - /** - * Returns the offsets delimiting the 'word' located at position offset. - * - * @param offset An offset in the text. - * @return The offsets for the start and end of the word located at <code>offset</code>. - * The two ints offsets are packed in a long using {@link #packRangeInLong(int, int)}. - * Returns -1 if no valid word was found. - */ - private long getWordLimitsAt(int offset) { - int klass = mInputType & InputType.TYPE_MASK_CLASS; - int variation = mInputType & InputType.TYPE_MASK_VARIATION; - - // Text selection is not permitted in password fields - if (hasPasswordTransformationMethod()) { - return -1; - } - - final int len = mText.length(); - - // Specific text fields: always select the entire text - if (klass == InputType.TYPE_CLASS_NUMBER || - klass == InputType.TYPE_CLASS_PHONE || - klass == InputType.TYPE_CLASS_DATETIME || - variation == InputType.TYPE_TEXT_VARIATION_URI || - variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || - variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS || - variation == InputType.TYPE_TEXT_VARIATION_FILTER) { - return len > 0 ? packRangeInLong(0, len) : -1; - } - - int end = Math.min(offset, len); - if (end < 0) { - return -1; - } - - final int MAX_LENGTH = 48; - int start = end; - - for (; start > 0; start--) { - final char c = mTransformed.charAt(start - 1); - final int type = Character.getType(c); - if (start == end && type == Character.OTHER_PUNCTUATION) { - // Cases where the text ends with a '.' and we select from the end of the line - // (right after the dot), or when we select from the space character in "aaa, bbb". - continue; - } - if (type == Character.SURROGATE) { // Two Character codepoint - end = start - 1; // Recheck as a pair when scanning forward - continue; - } - if (!isWordCharacter(c, type)) break; - if ((end - start) > MAX_LENGTH) return -1; - } - - for (; end < len; end++) { - final int c = Character.codePointAt(mTransformed, end); - final int type = Character.getType(c); - if (!isWordCharacter(c, type)) break; - if ((end - start) > MAX_LENGTH) return -1; - if (c > 0xFFFF) { // Two Character codepoint - end++; - } - } - - if (start == end) { - return -1; - } - - // Two ints packed in a long - return packRangeInLong(start, end); - } - private static long packRangeInLong(int start, int end) { return (((long) start) << 32) | end; } @@ -7837,21 +7806,40 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return (int) (range & 0x00000000FFFFFFFFL); } - private void selectAll() { - Selection.setSelection((Spannable) mText, 0, mText.length()); + private boolean selectAll() { + final int length = mText.length(); + Selection.setSelection((Spannable) mText, 0, length); + return length > 0; } - private void selectCurrentWord() { + /** + * Adjusts selection to the word under last touch offset. + * Return true if the operation was successfully performed. + */ + private boolean selectCurrentWord() { if (!canSelectText()) { - return; + return false; } if (hasPasswordTransformationMethod()) { // Always select all on a password field. // Cut/copy menu entries are not available for passwords, but being able to select all // is however useful to delete or paste to replace the entire content. - selectAll(); - return; + return selectAll(); + } + + int klass = mInputType & InputType.TYPE_MASK_CLASS; + int variation = mInputType & InputType.TYPE_MASK_VARIATION; + + // Specific text field types: select the entire text for these + if (klass == InputType.TYPE_CLASS_NUMBER || + klass == InputType.TYPE_CLASS_PHONE || + klass == InputType.TYPE_CLASS_DATETIME || + variation == InputType.TYPE_TEXT_VARIATION_URI || + variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || + variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS || + variation == InputType.TYPE_TEXT_VARIATION_FILTER) { + return selectAll(); } long lastTouchOffsets = getLastTouchOffsets(); @@ -7867,22 +7855,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener selectionStart = ((Spanned) mText).getSpanStart(url); selectionEnd = ((Spanned) mText).getSpanEnd(url); } else { - long wordLimits = getWordLimitsAt(minOffset); - if (wordLimits >= 0) { - selectionStart = extractRangeStartFromLong(wordLimits); - } else { - selectionStart = Math.max(minOffset - 5, 0); + if (mWordIterator == null) { + mWordIterator = new WordIterator(); } + // WordIerator handles text changes, this is a no-op if text in unchanged. + mWordIterator.setCharSequence(mText); - wordLimits = getWordLimitsAt(maxOffset); - if (wordLimits >= 0) { - selectionEnd = extractRangeEndFromLong(wordLimits); - } else { - selectionEnd = Math.min(maxOffset + 5, mText.length()); - } + selectionStart = mWordIterator.getBeginning(minOffset); + if (selectionStart == BreakIterator.DONE) return false; + + selectionEnd = mWordIterator.getEnd(maxOffset); + if (selectionEnd == BreakIterator.DONE) return false; } Selection.setSelection((Spannable) mText, selectionStart, selectionEnd); + return true; } private long getLastTouchOffsets() { @@ -7901,28 +7888,38 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - if (!isShown()) { - return false; - } + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); final boolean isPassword = hasPasswordTransformationMethod(); - if (!isPassword) { CharSequence text = getText(); if (TextUtils.isEmpty(text)) { text = getHint(); } if (!TextUtils.isEmpty(text)) { - if (text.length() > AccessibilityEvent.MAX_TEXT_LENGTH) { - text = text.subSequence(0, AccessibilityEvent.MAX_TEXT_LENGTH + 1); - } event.getText().add(text); } - } else { - event.setPassword(isPassword); } - return false; + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + final boolean isPassword = hasPasswordTransformationMethod(); + event.setPassword(isPassword); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + final boolean isPassword = hasPasswordTransformationMethod(); + if (!isPassword) { + info.setText(getText()); + } + info.setPassword(isPassword); } void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, @@ -8010,6 +8007,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * this will be {@link android.R.id#copyUrl}, {@link android.R.id#selectTextMode}, * {@link android.R.id#selectAll}, {@link android.R.id#paste}, {@link android.R.id#cut} * or {@link android.R.id#copy}. + * + * @return true if the context menu item action was performed. */ public boolean onTextContextMenuItem(int id) { int min = 0; @@ -8046,7 +8045,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(); } @@ -8054,7 +8053,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener case ID_SELECT_ALL: // This does not enter text selection mode. Text is highlighted, so that it can be - // bulk edited, like selectAllOnFocus does. + // bulk edited, like selectAllOnFocus does. Returns true even if text is empty. selectAll(); return true; @@ -8178,10 +8177,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Long press in empty space moves cursor and shows the Paste affordance if available. if (!isPositionOnText(mLastDownPositionX, mLastDownPositionY) && mInsertionControllerEnabled) { - final int offset = getOffset(mLastDownPositionX, mLastDownPositionY); + final int offset = getOffsetForPosition(mLastDownPositionX, mLastDownPositionY); stopSelectionActionMode(); - Selection.setSelection((Spannable)mText, offset); - getInsertionController().show(0); + Selection.setSelection((Spannable) mText, offset); + getInsertionController().showWithPaste(); handled = true; } @@ -8196,8 +8195,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; } @@ -8213,17 +8211,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(); @@ -8246,6 +8233,460 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); } + private static class SuggestionRangeSpan extends UnderlineSpan { + // TODO themable, would be nice to make it a child class of TextAppearanceSpan, but + // there is no way to have underline and TextAppearanceSpan. + } + + private class SuggestionsPopupWindow implements OnClickListener { + private static final int MAX_NUMBER_SUGGESTIONS = 5; + private static final int NO_SUGGESTIONS = -1; + private final PopupWindow mContainer; + private final ViewGroup[] mSuggestionViews = new ViewGroup[2]; + private final int[] mSuggestionViewLayouts = new int[] { + mTextEditSuggestionsBottomWindowLayout, mTextEditSuggestionsTopWindowLayout}; + private WordIterator mSuggestionWordIterator; + private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0]; + + public SuggestionsPopupWindow() { + mContainer = new PopupWindow(TextView.this.mContext, null, + com.android.internal.R.attr.textSuggestionsWindowStyle); + mContainer.setSplitTouchEnabled(true); + mContainer.setClippingEnabled(false); + mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); + + mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + } + + private class SuggestionInfo { + int suggestionStart, suggestionEnd; // range of suggestion item with replacement text + int spanStart, spanEnd; // range in TextView where text should be inserted + SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents + int suggestionIndex; // the index of the suggestion inside suggestionSpan + } + + private ViewGroup getViewGroup(boolean under) { + final int viewIndex = under ? 0 : 1; + ViewGroup viewGroup = mSuggestionViews[viewIndex]; + + if (viewGroup == null) { + final int layout = mSuggestionViewLayouts[viewIndex]; + LayoutInflater inflater = (LayoutInflater) TextView.this.mContext. + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + if (inflater == null) { + throw new IllegalArgumentException( + "Unable to create TextEdit suggestion window inflater"); + } + + View view = inflater.inflate(layout, null); + + if (! (view instanceof ViewGroup)) { + throw new IllegalArgumentException( + "Inflated TextEdit suggestion window is not a ViewGroup: " + view); + } + + viewGroup = (ViewGroup) view; + + // Inflate the suggestion items once and for all. + for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) { + View childView = inflater.inflate(mTextEditSuggestionItemLayout, viewGroup, + false); + + if (! (childView instanceof TextView)) { + throw new IllegalArgumentException( + "Inflated TextEdit suggestion item is not a TextView: " + childView); + } + + childView.setTag(new SuggestionInfo()); + viewGroup.addView(childView); + childView.setOnClickListener(this); + } + + mSuggestionViews[viewIndex] = viewGroup; + } + + return viewGroup; + } + + public void show() { + if (!(mText instanceof Editable)) return; + + final int pos = TextView.this.getSelectionStart(); + Spannable spannable = (Spannable)TextView.this.mText; + SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class); + final int nbSpans = suggestionSpans.length; + + ViewGroup viewGroup = getViewGroup(true); + mContainer.setContentView(viewGroup); + + int totalNbSuggestions = 0; + int spanUnionStart = mText.length(); + int spanUnionEnd = 0; + + for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { + SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; + final int spanStart = spannable.getSpanStart(suggestionSpan); + final int spanEnd = spannable.getSpanEnd(suggestionSpan); + spanUnionStart = Math.min(spanStart, spanUnionStart); + spanUnionEnd = Math.max(spanEnd, spanUnionEnd); + + String[] suggestions = suggestionSpan.getSuggestions(); + int nbSuggestions = suggestions.length; + for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { + TextView textView = (TextView) viewGroup.getChildAt(totalNbSuggestions); + textView.setText(suggestions[suggestionIndex]); + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + suggestionInfo.spanStart = spanStart; + suggestionInfo.spanEnd = spanEnd; + suggestionInfo.suggestionSpan = suggestionSpan; + suggestionInfo.suggestionIndex = suggestionIndex; + + totalNbSuggestions++; + if (totalNbSuggestions == MAX_NUMBER_SUGGESTIONS) { + // Also end outer for loop + spanIndex = nbSpans; + break; + } + } + } + + if (totalNbSuggestions == 0) { + // TODO Replace by final text, use a dedicated layout, add a fade out timer... + TextView textView = (TextView) viewGroup.getChildAt(0); + textView.setText("No suggestions available"); + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + suggestionInfo.spanStart = NO_SUGGESTIONS; + totalNbSuggestions++; + } else { + if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan(); + ((Editable) mText).setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + for (int i = 0; i < totalNbSuggestions; i++) { + final TextView textView = (TextView) viewGroup.getChildAt(i); + highlightTextDifferences(textView, spanUnionStart, spanUnionEnd); + } + } + + for (int i = 0; i < MAX_NUMBER_SUGGESTIONS; i++) { + viewGroup.getChildAt(i).setVisibility(i < totalNbSuggestions ? VISIBLE : GONE); + } + + final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + viewGroup.measure(size, size); + + positionAtCursor(); + } + + private long[] getWordLimits(CharSequence text) { + // TODO locale for mSuggestionWordIterator + if (mSuggestionWordIterator == null) mSuggestionWordIterator = new WordIterator(); + mSuggestionWordIterator.setCharSequence(text); + + // First pass will simply count the number of words to be able to create an array + // Not too expensive since previous break positions are cached by the BreakIterator + int nbWords = 0; + int position = mSuggestionWordIterator.following(0); + while (position != BreakIterator.DONE) { + nbWords++; + position = mSuggestionWordIterator.following(position); + } + + int index = 0; + long[] result = new long[nbWords]; + + position = mSuggestionWordIterator.following(0); + while (position != BreakIterator.DONE) { + int wordStart = mSuggestionWordIterator.getBeginning(position); + result[index++] = packRangeInLong(wordStart, position); + position = mSuggestionWordIterator.following(position); + } + + return result; + } + + private TextAppearanceSpan highlightSpan(int index) { + final int length = mHighlightSpans.length; + if (index < length) { + return mHighlightSpans[index]; + } + + // Assumes indexes are requested in sequence: simply append one more item + TextAppearanceSpan[] newArray = new TextAppearanceSpan[length + 1]; + System.arraycopy(mHighlightSpans, 0, newArray, 0, length); + TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext, + android.R.style.TextAppearance_SuggestionHighlight); + newArray[length] = highlightSpan; + mHighlightSpans = newArray; + return highlightSpan; + } + + private void highlightTextDifferences(TextView textView, int unionStart, int unionEnd) { + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + final int spanStart = suggestionInfo.spanStart; + final int spanEnd = suggestionInfo.spanEnd; + + // Remove all text formating by converting to Strings + final String text = textView.getText().toString(); + final String sourceText = mText.subSequence(spanStart, spanEnd).toString(); + + long[] sourceWordLimits = getWordLimits(sourceText); + long[] wordLimits = getWordLimits(text); + + SpannableStringBuilder ssb = new SpannableStringBuilder(); + // span [spanStart, spanEnd] is included in union [spanUnionStart, int spanUnionEnd] + // The final result is made of 3 parts: the text before, between and after the span + // This is the text before, provided for context + ssb.append(mText.subSequence(unionStart, spanStart).toString()); + + // shift is used to offset spans positions wrt span's beginning + final int shift = spanStart - unionStart; + suggestionInfo.suggestionStart = shift; + suggestionInfo.suggestionEnd = shift + text.length(); + + // This is the actual suggestion text, which will be highlighted by the following code + ssb.append(text); + + String[] words = new String[wordLimits.length]; + for (int i = 0; i < wordLimits.length; i++) { + int wordStart = extractRangeStartFromLong(wordLimits[i]); + int wordEnd = extractRangeEndFromLong(wordLimits[i]); + words[i] = text.substring(wordStart, wordEnd); + } + + // Highlighted word algorithm is based on word matching between source and text + // Matching words are found from left to right. TODO: change for RTL languages + // Characters between matching words are highlighted + int previousCommonWordIndex = -1; + int nbHighlightSpans = 0; + for (int i = 0; i < sourceWordLimits.length; i++) { + int wordStart = extractRangeStartFromLong(sourceWordLimits[i]); + int wordEnd = extractRangeEndFromLong(sourceWordLimits[i]); + String sourceWord = sourceText.substring(wordStart, wordEnd); + + for (int j = previousCommonWordIndex + 1; j < words.length; j++) { + if (sourceWord.equals(words[j])) { + if (j != previousCommonWordIndex + 1) { + int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 : + extractRangeEndFromLong(wordLimits[previousCommonWordIndex]); + int lastDifferentPosition = extractRangeStartFromLong(wordLimits[j]); + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + firstDifferentPosition, shift + lastDifferentPosition, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + // Compare characters between words + int previousSourceWordEnd = i == 0 ? 0 : + extractRangeEndFromLong(sourceWordLimits[i - 1]); + int sourceWordStart = extractRangeStartFromLong(sourceWordLimits[i]); + String sourceSpaces = sourceText.substring(previousSourceWordEnd, + sourceWordStart); + + int previousWordEnd = j == 0 ? 0 : + extractRangeEndFromLong(wordLimits[j - 1]); + int currentWordStart = extractRangeStartFromLong(wordLimits[j]); + String textSpaces = text.substring(previousWordEnd, currentWordStart); + + if (!sourceSpaces.equals(textSpaces)) { + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + previousWordEnd, shift + currentWordStart, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + previousCommonWordIndex = j; + break; + } + } + } + + // Finally, compare ends of Strings + if (previousCommonWordIndex < words.length - 1) { + int firstDifferentPosition = previousCommonWordIndex < 0 ? 0 : + extractRangeEndFromLong(wordLimits[previousCommonWordIndex]); + int lastDifferentPosition = textView.length(); + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + firstDifferentPosition, shift + lastDifferentPosition, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + int lastSourceWordEnd = sourceWordLimits.length == 0 ? 0 : + extractRangeEndFromLong(sourceWordLimits[sourceWordLimits.length - 1]); + String sourceSpaces = sourceText.substring(lastSourceWordEnd, sourceText.length()); + + int lastCommonTextWordEnd = previousCommonWordIndex < 0 ? 0 : + extractRangeEndFromLong(wordLimits[previousCommonWordIndex]); + String textSpaces = text.substring(lastCommonTextWordEnd, textView.length()); + + if (!sourceSpaces.equals(textSpaces) && textSpaces.length() > 0) { + ssb.setSpan(highlightSpan(nbHighlightSpans++), + shift + lastCommonTextWordEnd, shift + textView.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + // Final part, text after the current suggestion range. + ssb.append(mText.subSequence(spanEnd, unionEnd).toString()); + textView.setText(ssb); + } + + public void hide() { + if ((mText instanceof Editable) && mSuggestionRangeSpan != null) { + ((Editable) mText).removeSpan(mSuggestionRangeSpan); + } + mContainer.dismiss(); + } + + @Override + public void onClick(View view) { + if (view instanceof TextView) { + TextView textView = (TextView) view; + SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); + final int spanStart = suggestionInfo.spanStart; + final int spanEnd = suggestionInfo.spanEnd; + if (spanStart != NO_SUGGESTIONS) { + // SuggestionSpans are removed by replace: save them before + Editable editable = ((Editable) mText); + SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd, + SuggestionSpan.class); + final int length = suggestionSpans.length; + int[] suggestionSpansStarts = new int[length]; + int[] suggestionSpansEnds = new int[length]; + int[] suggestionSpansFlags = new int[length]; + for (int i = 0; i < length; i++) { + final SuggestionSpan suggestionSpan = suggestionSpans[i]; + suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan); + suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan); + suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan); + } + + final int suggestionStart = suggestionInfo.suggestionStart; + final int suggestionEnd = suggestionInfo.suggestionEnd; + final String suggestion = textView.getText().subSequence( + suggestionStart, suggestionEnd).toString(); + final String originalText = mText.subSequence(spanStart, spanEnd).toString(); + ((Editable) mText).replace(spanStart, spanEnd, suggestion); + + // Swap text content between actual text and Suggestion span + String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions(); + suggestions[suggestionInfo.suggestionIndex] = originalText; + + // Notify source IME of the suggestion pick + if (!TextUtils.isEmpty( + suggestionInfo.suggestionSpan.getNotificationTargetClassName())) { + InputMethodManager imm = InputMethodManager.peekInstance(); + imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText, + suggestionInfo.suggestionIndex); + } + + // Restore previous SuggestionSpans + final int lengthDifference = suggestion.length() - (spanEnd - spanStart); + for (int i = 0; i < length; i++) { + // Only spans that include the modified region make sense after replacement + // Spans partially included in the replaced region are removed, there is no + // way to assign them a valid range after replacement + if (suggestionSpansStarts[i] <= spanStart && + suggestionSpansEnds[i] >= spanEnd) { + editable.setSpan(suggestionSpans[i], suggestionSpansStarts[i], + suggestionSpansEnds[i] + lengthDifference, + suggestionSpansFlags[i]); + } + } + } + } + hide(); + } + + void positionAtCursor() { + View contentView = mContainer.getContentView(); + int width = contentView.getMeasuredWidth(); + int height = contentView.getMeasuredHeight(); + final int offset = TextView.this.getSelectionStart(); + final int line = mLayout.getLineForOffset(offset); + final int lineBottom = mLayout.getLineBottom(line); + float primaryHorizontal = mLayout.getPrimaryHorizontal(offset); + + final Rect bounds = sCursorControllerTempRect; + bounds.left = (int) (primaryHorizontal - width / 2.0f); + bounds.top = lineBottom; + + bounds.right = bounds.left + width; + bounds.bottom = bounds.top + height; + + convertFromViewportToContentCoordinates(bounds); + + final int[] coords = mTempCoords; + TextView.this.getLocationInWindow(coords); + coords[0] += bounds.left; + coords[1] += bounds.top; + + final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics(); + final int screenHeight = displayMetrics.heightPixels; + + // Vertical clipping + if (coords[1] + height > screenHeight) { + // Try to position above current line instead + // TODO use top layout instead, reverse suggestion order, + // try full screen vertical down if it still does not fit. TBD with designers. + + // Update dimensions from new view + contentView = mContainer.getContentView(); + width = contentView.getMeasuredWidth(); + height = contentView.getMeasuredHeight(); + + final int lineTop = mLayout.getLineTop(line); + final int lineHeight = lineBottom - lineTop; + coords[1] -= height + lineHeight; + } + + // Horizontal clipping + coords[0] = Math.max(0, coords[0]); + coords[0] = Math.min(displayMetrics.widthPixels - width, coords[0]); + + mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY, coords[0], coords[1]); + } + } + + void showSuggestions() { + if (!mSuggestionsEnabled || !isTextEditable()) return; + + if (mSuggestionsPopupWindow == null) { + mSuggestionsPopupWindow = new SuggestionsPopupWindow(); + } + hideControllers(); + mSuggestionsPopupWindow.show(); + } + + void hideSuggestions() { + if (mSuggestionsPopupWindow != null) { + mSuggestionsPopupWindow.hide(); + } + } + + /** + * Some parts of the text can have alternate suggestion text attached. This is typically done by + * the IME by adding {@link SuggestionSpan}s to the text. + * + * When suggestions are enabled (default), this list of suggestions will be displayed when the + * user double taps on these parts of the text. No suggestions are displayed when this value is + * false. Use {@link #setSuggestionsEnabled(boolean)} to change this value. + * + * @return true if the suggestions popup window is enabled. + * + * @attr ref android.R.styleable#TextView_suggestionsEnabled + */ + public boolean isSuggestionsEnabled() { + return mSuggestionsEnabled; + } + + /** + * Enables or disables the suggestion popup. See {@link #isSuggestionsEnabled()}. + * + * @param enabled Whether or not suggestions are enabled. + */ + public void setSuggestionsEnabled(boolean enabled) { + mSuggestionsEnabled = enabled; + } + /** * If provided, this ActionMode.Callback will be used to create the ActionMode when text * selection is initiated in this View. @@ -8298,8 +8739,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (!hasSelection()) { - // If selection mode is started after a device rotation, there is already a selection. - selectCurrentWord(); + // There may already be a selection on device rotation + boolean currentWordSelected = selectCurrentWord(); + if (!currentWordSelected) { + // No word found under cursor or text selection not permitted. + return false; + } } ActionMode.Callback actionModeCallback = new SelectionActionModeCallback(); @@ -8330,16 +8775,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = clipboard.getPrimaryClip(); if (clip != null) { - boolean didfirst = false; + boolean didFirst = false; for (int i=0; i<clip.getItemCount(); i++) { CharSequence paste = clip.getItemAt(i).coerceToText(getContext()); if (paste != null) { - if (!didfirst) { + if (!didFirst) { long minMax = prepareSpacesAroundPaste(min, max, paste); min = extractRangeStartFromLong(minMax); max = extractRangeEndFromLong(minMax); Selection.setSelection((Spannable) mText, max); ((Editable) mText).replace(min, max, paste); + didFirst = true; } else { ((Editable) mText).insert(getSelectionEnd(), "\n"); ((Editable) mText).insert(getSelectionEnd(), paste); @@ -8370,10 +8816,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean onCreateActionMode(ActionMode mode, Menu menu) { TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme); - mode.setTitle(mContext.getString(com.android.internal.R.string.textSelectionCABTitle)); + boolean allowText = getContext().getResources().getBoolean( + com.android.internal.R.bool.allow_action_menu_item_text_with_icon); + + mode.setTitle(allowText ? + mContext.getString(com.android.internal.R.string.textSelectionCABTitle) : null); mode.setSubtitle(null); + int selectAllIconId = 0; // No icon by default + if (!allowText) { + // Provide an icon, text will not be displayed on smaller screens. + selectAllIconId = styledAttributes.getResourceId( + R.styleable.Theme_actionModeSelectAllDrawable, 0); + } + menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). + setIcon(selectAllIconId). setAlphabeticShortcut('a'). setShowAsAction( MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); @@ -8454,65 +8912,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); @@ -8595,14 +9002,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) { @@ -8638,30 +9041,54 @@ 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; - private PastePopupMenu mPastePopupWindow; + // Used to detect that setFrame was called + private boolean mNeedsUpdate = true; + + 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; + } + + @Override + protected boolean setFrame(int left, int top, int right, int bottom) { + boolean changed = super.setFrame(left, top, right, bottom); + // onPreDraw is called for PhoneWindow before the layout of this view is + // performed. Make sure to update position, even if container didn't move. + if (changed) mNeedsUpdate = true; + return changed; + } + + protected abstract void initDrawable(); // Touch-up filter: number of previous positions remembered private static final int HISTORY_SIZE = 5; @@ -8702,71 +9129,8 @@ 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]); - } - } - - 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; - } + updateOffset(mPreviousOffsets[index]); } - - final int handleHeight = mDrawable.getIntrinsicHeight(); - mTouchOffsetY = -0.3f * handleHeight; - mIdealVerticalOffset = 0.7f * handleHeight; - - invalidate(); } @Override @@ -8775,12 +9139,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } public void show() { - updateContainerPosition(); if (isShowing()) { mContainer.update(mContainerPositionX, mContainerPositionY, mRight - mLeft, mBottom - mTop); - - hidePastePopupWindow(); } else { mContainer.showAtLocation(TextView.this, 0, mContainerPositionX, mContainerPositionY); @@ -8792,10 +9153,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void dismiss() { + protected void dismiss() { mIsDragging = false; mContainer.dismiss(); - hidePastePopupWindow(); } public void hide() { @@ -8826,24 +9186,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; @@ -8852,45 +9210,60 @@ 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(float x, float y); + + protected void positionAtCursorOffset(int offset) { + // A HandleView relies on the layout, which may be nulled by external methods. + if (mLayout == null) { + // Will update controllers' state, hiding them and stopping selection mode if needed + prepareCursorControllers(); + return; } + + 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)); + private void checkForContainerPositionChange() { + 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; + mNeedsUpdate |= previousContainerPositionX != mContainerPositionX; + mNeedsUpdate |= previousContainerPositionY != mContainerPositionY; } public boolean onPreDraw() { - if (updateContainerPosition()) { + checkForContainerPositionChange(); + if (mNeedsUpdate) { + 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); @@ -8903,9 +9276,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener dismiss(); } } - - // Hide paste popup as soon as the view is scrolled or moved - hidePastePopupWindow(); + mNeedsUpdate = false; } return true; } @@ -8920,11 +9291,9 @@ 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); @@ -8954,24 +9323,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(newPosX, newPosY); break; } case MotionEvent.ACTION_UP: - if (mIsInsertionHandle) { - 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; @@ -8987,60 +9343,35 @@ 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 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; @@ -9049,81 +9380,252 @@ 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(); + break; + + case MotionEvent.ACTION_UP: + 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(float x, float y) { + updateOffset(getOffsetForPosition(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(); + } + + void hidePastePopupWindow() { + if (mPastePopupShower != null) { + TextView.this.removeCallbacks(mPastePopupShower); + } + if (mPastePopupWindow != null) { + mPastePopupWindow.hide(); + } } - public void updatePosition() { - final int offset = getSelectionStart(); + @Override + public void onDetached() { + removeHiderCallback(); + hidePastePopupWindow(); + } + } - if (offset < 0) { - // Should never happen, safety check. - Log.w(LOG_TAG, "Update cursor controller position called with no cursor"); - hide(); - return; + 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(float x, float y) { + final int selectionStart = getSelectionStart(); + final int selectionEnd = getSelectionEnd(); + + int offset = getOffsetForPosition(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(float x, float y) { + final int selectionStart = getSelectionStart(); + final int selectionEnd = getSelectionEnd(); + + int offset = getOffsetForPosition(x, y); + + // No need to redraw when the offset is unchanged + if (offset == selectionEnd) return; + // Handles can not cross and selection is at least one character + if (offset <= selectionStart) offset = selectionStart + 1; + + Selection.setSelection((Spannable) mText, selectionStart, offset); + } + } + + /** + * A CursorController instance can be used to control a cursor in the text. + * It is not used outside of {@link TextView}. + * @hide + */ + private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { + /** + * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. + * See also {@link #hide()}. + */ + public void show(); - getHandle().positionAtCursor(offset); + /** + * Hide the cursor controller from screen. + * See also {@link #show()}. + */ + public void hide(); + + /** + * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller + * a chance to become active and/or visible. + * @param event The touch event + */ + public boolean onTouchEvent(MotionEvent event); + + /** + * Called when the view is detached from window. Perform house keeping task, such as + * stopping Runnable thread that would otherwise keep a reference on the context, thus + * preventing the activity from being recycled. + */ + public void onDetached(); + } + + private class InsertionPointCursorController implements CursorController { + private static final int DELAY_BEFORE_PASTE = 2000; + + private InsertionHandleView mHandle; + + public void show() { + ((InsertionHandleView) getHandle()).show(DELAY_BEFORE_PASTE); + } + + public 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) { @@ -9138,30 +9640,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 float mPreviousTapPositionX, mPreviousTapPositionY; SelectionModifierCursorController() { resetTouchOffsets(); @@ -9173,96 +9675,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) { @@ -9271,21 +9696,21 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (isTextEditable() || mTextIsSelectable) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: - final int x = (int) event.getX(); - final int y = (int) event.getY(); + final float x = event.getX(); + final float y = event.getY(); // Remember finger down position, to be able to start selection from there - mMinTouchOffset = mMaxTouchOffset = getOffset(x, y); + mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y); // Double tap detection long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime; if (duration <= ViewConfiguration.getDoubleTapTimeout() && isPositionOnText(x, y)) { - final int deltaX = x - mPreviousTapPositionX; - final int deltaY = y - mPreviousTapPositionY; - final int distanceSquared = deltaX * deltaX + deltaY * deltaY; + final float deltaX = x - mPreviousTapPositionX; + final float deltaY = y - mPreviousTapPositionY; + final float distanceSquared = deltaX * deltaX + deltaY * deltaY; if (distanceSquared < mSquaredTouchSlopDistance) { - startSelectionActionMode(); + showSuggestions(); mDiscardNextActionUp = true; } } @@ -9319,9 +9744,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private void updateMinAndMaxOffsets(MotionEvent event) { int pointerCount = event.getPointerCount(); for (int index = 0; index < pointerCount; index++) { - final int x = (int) event.getX(index); - final int y = (int) event.getY(index); - int offset = getOffset(x, y); + int offset = getOffsetForPosition(event.getX(index), event.getY(index)); if (offset < mMinTouchOffset) mMinTouchOffset = offset; if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; } @@ -9354,7 +9777,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public void onDetached() { - // Nothing to do + final ViewTreeObserver observer = getViewTreeObserver(); + observer.removeOnTouchModeChangeListener(this); + + if (mStartHandle != null) mStartHandle.onDetached(); + if (mEndHandle != null) mEndHandle.onDetached(); } } @@ -9371,44 +9798,44 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private void hideControllers() { hideInsertionPointCursorController(); stopSelectionActionMode(); + hideSuggestions(); } /** - * Get the offset character closest to the specified absolute position. + * Get the character offset closest to the specified absolute position. A typical use case is to + * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method. * * @param x The horizontal absolute position of a point on screen * @param y The vertical absolute position of a point on screen * @return the character offset for the character whose position is closest to the specified * position. Returns -1 if there is no layout. - * - * @hide */ - public int getOffset(int x, int y) { + public int getOffsetForPosition(float x, float y) { if (getLayout() == null) return -1; final int line = getLineAtCoordinate(y); final int offset = getOffsetAtCoordinate(line, x); return offset; } - private int convertToLocalHorizontalCoordinate(int x) { + private float convertToLocalHorizontalCoordinate(float x) { x -= getTotalPaddingLeft(); // Clamp the position to inside of the view. - x = Math.max(0, x); + x = Math.max(0.0f, x); x = Math.min(getWidth() - getTotalPaddingRight() - 1, x); x += getScrollX(); return x; } - private int getLineAtCoordinate(int y) { + private int getLineAtCoordinate(float y) { y -= getTotalPaddingTop(); // Clamp the position to inside of the view. - y = Math.max(0, y); + y = Math.max(0.0f, y); y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y); y += getScrollY(); - return getLayout().getLineForVertical(y); + return getLayout().getLineForVertical((int) y); } - private int getOffsetAtCoordinate(int line, int x) { + private int getOffsetAtCoordinate(int line, float x) { x = convertToLocalHorizontalCoordinate(x); return getLayout().getOffsetForHorizontal(line, x); } @@ -9416,7 +9843,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed * in the view. Returns false when the position is in the empty space of left/right of text. */ - private boolean isPositionOnText(int x, int y) { + private boolean isPositionOnText(float x, float y) { if (getLayout() == null) return false; final int line = getLineAtCoordinate(y); @@ -9438,7 +9865,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return true; case DragEvent.ACTION_DRAG_LOCATION: - final int offset = getOffset((int) event.getX(), (int) event.getY()); + final int offset = getOffsetForPosition(event.getX(), event.getY()); Selection.setSelection((Spannable)mText, offset); return true; @@ -9462,7 +9889,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener content.append(item.coerceToText(TextView.this.mContext)); } - final int offset = getOffset((int) event.getX(), (int) event.getY()); + final int offset = getOffsetForPosition(event.getX(), event.getY()); Object localState = event.getLocalState(); DragLocalState dragLocalState = null; @@ -9617,7 +10044,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private boolean mSelectAllOnFocus = false; - private int mGravity = Gravity.TOP | Gravity.LEFT; + private int mGravity = Gravity.TOP | Gravity.START; private boolean mHorizontallyScrolling; private int mAutoLinkMask; diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index 029d690..423e735 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -409,7 +409,9 @@ public class TimePicker extends FrameLayout { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + int flags = DateUtils.FORMAT_SHOW_TIME; if (mIs24HourView) { flags |= DateUtils.FORMAT_24HOUR; @@ -421,7 +423,6 @@ public class TimePicker extends FrameLayout { String selectedDateUtterance = DateUtils.formatDateTime(mContext, mTempCalendar.getTimeInMillis(), flags); event.getText().add(selectedDateUtterance); - return true; } private void updateHourControl() { diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index 450c966..9e37c7b 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -33,7 +33,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; -import android.view.ViewRoot; +import android.view.ViewAncestor; import android.view.WindowManager; import android.view.View.OnClickListener; import android.view.WindowManager.LayoutParams; @@ -501,7 +501,7 @@ public class ZoomButtonsController implements View.OnTouchListener { } else { - ViewRoot viewRoot = getOwnerViewRoot(); + ViewAncestor viewRoot = getOwnerViewAncestor(); if (viewRoot != null) { viewRoot.dispatchKey(event); } @@ -526,15 +526,15 @@ public class ZoomButtonsController implements View.OnTouchListener { } } - private ViewRoot getOwnerViewRoot() { + private ViewAncestor getOwnerViewAncestor() { View rootViewOfOwner = mOwnerView.getRootView(); if (rootViewOfOwner == null) { return null; } ViewParent parentOfRootView = rootViewOfOwner.getParent(); - if (parentOfRootView instanceof ViewRoot) { - return (ViewRoot) parentOfRootView; + if (parentOfRootView instanceof ViewAncestor) { + return (ViewAncestor) parentOfRootView; } else { return null; } diff --git a/core/java/com/android/internal/app/ActionBarImpl.java b/core/java/com/android/internal/app/ActionBarImpl.java index 8f1354b..8d5df6f 100644 --- a/core/java/com/android/internal/app/ActionBarImpl.java +++ b/core/java/com/android/internal/app/ActionBarImpl.java @@ -16,24 +16,27 @@ package com.android.internal.app; +import com.android.internal.R; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuPopupHelper; import com.android.internal.view.menu.SubMenuBuilder; import com.android.internal.widget.ActionBarContainer; import com.android.internal.widget.ActionBarContextView; import com.android.internal.widget.ActionBarView; +import com.android.internal.widget.ScrollingTabContainerView; 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.content.res.Configuration; +import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.Handler; import android.view.ActionMode; @@ -43,9 +46,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.Window; -import android.view.animation.DecelerateInterpolator; -import android.widget.FrameLayout; -import android.widget.LinearLayout; import android.widget.SpinnerAdapter; import java.lang.ref.WeakReference; @@ -59,8 +59,7 @@ import java.util.ArrayList; * which is normally hidden. */ public class ActionBarImpl extends ActionBar { - private static final int NORMAL_VIEW = 0; - private static final int CONTEXT_VIEW = 1; + private static final String TAG = "ActionBarImpl"; private Context mContext; private Activity mActivity; @@ -68,9 +67,10 @@ public class ActionBarImpl extends ActionBar { private ActionBarContainer mContainerView; private ActionBarView mActionView; - private ActionBarContextView mUpperContextView; - private LinearLayout mLowerContextView; + private ActionBarContextView mContextView; + private ActionBarContainer mSplitView; private View mContentView; + private ScrollingTabContainerView mTabScrollView; private ArrayList<TabImpl> mTabs = new ArrayList<TabImpl>(); @@ -89,63 +89,18 @@ public class ActionBarImpl extends ActionBar { private static final int INVALID_POSITION = -1; private int mContextDisplayMode; + private boolean mHasEmbeddedTabs; + private int mContentHeight; final Handler mHandler = new Handler(); + Runnable mTabSelector; - private Animator mCurrentAnim; + private Animator mCurrentShowAnim; + private Animator mCurrentModeAnim; private boolean mShowHideAnimationEnabled; + boolean mWasHiddenBeforeMode; - private static final TimeInterpolator sFadeOutInterpolator = new DecelerateInterpolator(); - - final AnimatorListener[] mAfterAnimation = new AnimatorListener[] { - new AnimatorListener() { // NORMAL_VIEW - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - if (mLowerContextView != null) { - mLowerContextView.removeAllViews(); - } - mCurrentAnim = null; - hideAllExcept(NORMAL_VIEW); - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - }, - new AnimatorListener() { // CONTEXT_VIEW - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - mCurrentAnim = null; - hideAllExcept(CONTEXT_VIEW); - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - } - }; - - final AnimatorListener mHideListener = new AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - + final AnimatorListener mHideListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mContentView != null) { @@ -153,36 +108,16 @@ public class ActionBarImpl extends ActionBar { } mContainerView.setVisibility(View.GONE); mContainerView.setTransitioning(false); - mCurrentAnim = null; - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { + mCurrentShowAnim = null; } }; - final AnimatorListener mShowListener = new AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - + final AnimatorListener mShowListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - mCurrentAnim = null; + mCurrentShowAnim = null; mContainerView.requestLayout(); } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } }; public ActionBarImpl(Activity activity) { @@ -203,21 +138,69 @@ public class ActionBarImpl extends ActionBar { private void init(View decor) { mContext = decor.getContext(); mActionView = (ActionBarView) decor.findViewById(com.android.internal.R.id.action_bar); - mUpperContextView = (ActionBarContextView) decor.findViewById( + mContextView = (ActionBarContextView) decor.findViewById( com.android.internal.R.id.action_context_bar); - mLowerContextView = (LinearLayout) decor.findViewById( - com.android.internal.R.id.lower_action_context_bar); mContainerView = (ActionBarContainer) decor.findViewById( com.android.internal.R.id.action_bar_container); + mSplitView = (ActionBarContainer) decor.findViewById( + com.android.internal.R.id.split_action_bar); - if (mActionView == null || mUpperContextView == null || mContainerView == null) { + if (mActionView == null || mContextView == null || mContainerView == null) { throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + "with a compatible window decor layout"); } - mActionView.setContextView(mUpperContextView); - mContextDisplayMode = mLowerContextView == null ? - CONTEXT_DISPLAY_NORMAL : CONTEXT_DISPLAY_SPLIT; + mHasEmbeddedTabs = mContext.getResources().getBoolean( + com.android.internal.R.bool.action_bar_embed_tabs); + mActionView.setContextView(mContextView); + mContextDisplayMode = mActionView.isSplitActionBar() ? + CONTEXT_DISPLAY_SPLIT : CONTEXT_DISPLAY_NORMAL; + + mContentHeight = mActionView.getContentHeight(); + } + + public void onConfigurationChanged(Configuration newConfig) { + mHasEmbeddedTabs = mContext.getResources().getBoolean( + com.android.internal.R.bool.action_bar_embed_tabs); + + // Switch tab layout configuration if needed + if (!mHasEmbeddedTabs) { + mActionView.setEmbeddedTabView(null); + mContainerView.setTabContainer(mTabScrollView); + } else { + mContainerView.setTabContainer(null); + if (mTabScrollView != null) { + mTabScrollView.setVisibility(View.VISIBLE); + } + mActionView.setEmbeddedTabView(mTabScrollView); + } + mActionView.setCollapsable(!mHasEmbeddedTabs && + getNavigationMode() == NAVIGATION_MODE_TABS); + + mContentHeight = mActionView.getContentHeight(); + + if (mTabScrollView != null) { + mTabScrollView.getLayoutParams().height = mContentHeight; + mTabScrollView.requestLayout(); + } + } + + private void ensureTabsExist() { + if (mTabScrollView != null) { + return; + } + + ScrollingTabContainerView tabScroller = mActionView.createTabContainer(); + + if (mHasEmbeddedTabs) { + tabScroller.setVisibility(View.VISIBLE); + mActionView.setEmbeddedTabView(tabScroller); + } else { + tabScroller.setVisibility(getNavigationMode() == NAVIGATION_MODE_TABS ? + View.VISIBLE : View.GONE); + mContainerView.setTabContainer(tabScroller); + } + mTabScrollView = tabScroller; } /** @@ -229,8 +212,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 +268,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)); } @@ -317,7 +305,9 @@ public class ActionBarImpl extends ActionBar { selectTab(null); } mTabs.clear(); - mActionView.removeAllTabs(); + if (mTabScrollView != null) { + mTabScrollView.removeAllTabs(); + } mSavedTabPosition = INVALID_POSITION; } @@ -367,18 +357,18 @@ public class ActionBarImpl extends ActionBar { mActionMode.finish(); } - mUpperContextView.killMode(); - ActionMode mode = new ActionModeImpl(callback); - if (callback.onCreateActionMode(mode, mode.getMenu())) { + mContextView.killMode(); + ActionModeImpl mode = new ActionModeImpl(callback); + if (mode.dispatchOnCreate()) { + mWasHiddenBeforeMode = !isShowing(); mode.invalidate(); - mUpperContextView.initForMode(mode); - animateTo(CONTEXT_VIEW); - if (mLowerContextView != null) { + mContextView.initForMode(mode); + animateToMode(true); + if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) { // TODO animate this - mLowerContextView.setVisibility(View.VISIBLE); + mSplitView.setVisibility(View.VISIBLE); } mActionMode = mode; - show(); return mode; } return null; @@ -413,7 +403,8 @@ public class ActionBarImpl extends ActionBar { @Override public void addTab(Tab tab, boolean setSelected) { - mActionView.addTab(tab, setSelected); + ensureTabsExist(); + mTabScrollView.addTab(tab, setSelected); configureTab(tab, mTabs.size()); if (setSelected) { selectTab(tab); @@ -422,7 +413,8 @@ public class ActionBarImpl extends ActionBar { @Override public void addTab(Tab tab, int position, boolean setSelected) { - mActionView.addTab(tab, position, setSelected); + ensureTabsExist(); + mTabScrollView.addTab(tab, position, setSelected); configureTab(tab, position); if (setSelected) { selectTab(tab); @@ -441,10 +433,18 @@ public class ActionBarImpl extends ActionBar { @Override public void removeTabAt(int position) { + if (mTabScrollView == null) { + // No tabs around to remove + return; + } + int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : mSavedTabPosition; - mActionView.removeTabAt(position); - mTabs.remove(position); + mTabScrollView.removeTabAt(position); + TabImpl removedTab = mTabs.remove(position); + if (removedTab != null) { + removedTab.setPosition(-1); + } final int newTabCount = mTabs.size(); for (int i = position; i < newTabCount; i++) { @@ -469,9 +469,10 @@ public class ActionBarImpl extends ActionBar { if (mSelectedTab == tab) { if (mSelectedTab != null) { mSelectedTab.getCallback().onTabReselected(mSelectedTab, trans); + mTabScrollView.animateToTab(tab.getPosition()); } } else { - mActionView.setTabSelected(tab != null ? tab.getPosition() : Tab.INVALID_POSITION); + mTabScrollView.setTabSelected(tab != null ? tab.getPosition() : Tab.INVALID_POSITION); if (mSelectedTab != null) { mSelectedTab.getCallback().onTabUnselected(mSelectedTab, trans); } @@ -498,10 +499,15 @@ public class ActionBarImpl extends ActionBar { @Override public void show() { - if (mCurrentAnim != null) { - mCurrentAnim.end(); + show(true); + } + + void show(boolean markHiddenBeforeMode) { + if (mCurrentShowAnim != null) { + mCurrentShowAnim.end(); } if (mContainerView.getVisibility() == View.VISIBLE) { + if (markHiddenBeforeMode) mWasHiddenBeforeMode = false; return; } mContainerView.setVisibility(View.VISIBLE); @@ -516,18 +522,24 @@ public class ActionBarImpl extends ActionBar { mContainerView.setTranslationY(-mContainerView.getHeight()); b.with(ObjectAnimator.ofFloat(mContainerView, "translationY", 0)); } + if (mSplitView != null && mContextDisplayMode == CONTEXT_DISPLAY_SPLIT) { + mSplitView.setAlpha(0); + b.with(ObjectAnimator.ofFloat(mSplitView, "alpha", 1)); + } anim.addListener(mShowListener); - mCurrentAnim = anim; + mCurrentShowAnim = anim; anim.start(); } else { + mContainerView.setAlpha(1); + mContainerView.setTranslationY(0); mShowListener.onAnimationEnd(null); } } @Override public void hide() { - if (mCurrentAnim != null) { - mCurrentAnim.end(); + if (mCurrentShowAnim != null) { + mCurrentShowAnim.end(); } if (mContainerView.getVisibility() == View.GONE) { return; @@ -544,8 +556,12 @@ public class ActionBarImpl extends ActionBar { b.with(ObjectAnimator.ofFloat(mContainerView, "translationY", -mContainerView.getHeight())); } + if (mSplitView != null && mSplitView.getVisibility() == View.VISIBLE) { + mSplitView.setAlpha(1); + b.with(ObjectAnimator.ofFloat(mSplitView, "alpha", 0)); + } anim.addListener(mHideListener); - mCurrentAnim = anim; + mCurrentShowAnim = anim; anim.start(); } else { mHideListener.onAnimationEnd(null); @@ -556,41 +572,14 @@ public class ActionBarImpl extends ActionBar { return mContainerView.getVisibility() == View.VISIBLE; } - private long animateTo(int viewIndex) { - show(); - - AnimatorSet set = new AnimatorSet(); - - final View targetChild = mContainerView.getChildAt(viewIndex); - targetChild.setVisibility(View.VISIBLE); - AnimatorSet.Builder b = set.play(ObjectAnimator.ofFloat(targetChild, "alpha", 1)); - - final int count = mContainerView.getChildCount(); - for (int i = 0; i < count; i++) { - final View child = mContainerView.getChildAt(i); - if (i == viewIndex) { - continue; - } - - if (child.getVisibility() != View.GONE) { - Animator a = ObjectAnimator.ofFloat(child, "alpha", 0); - a.setInterpolator(sFadeOutInterpolator); - b.with(a); - } + void animateToMode(boolean toActionMode) { + show(false); + if (mCurrentModeAnim != null) { + mCurrentModeAnim.end(); } - set.addListener(mAfterAnimation[viewIndex]); - - mCurrentAnim = set; - set.start(); - return set.getDuration(); - } - - private void hideAllExcept(int viewIndex) { - final int count = mContainerView.getChildCount(); - for (int i = 0; i < count; i++) { - mContainerView.getChildAt(i).setVisibility(i == viewIndex ? View.VISIBLE : View.GONE); - } + mActionView.animateToVisibility(toActionMode ? View.GONE : View.VISIBLE); + mContextView.animateToVisibility(toActionMode ? View.VISIBLE : View.GONE); } /** @@ -627,38 +616,50 @@ public class ActionBarImpl extends ActionBar { mCallback.onDestroyActionMode(this); mCallback = null; - animateTo(NORMAL_VIEW); + animateToMode(false); // Clear out the context mode views after the animation finishes - mUpperContextView.closeMode(); - if (mLowerContextView != null && mLowerContextView.getVisibility() != View.GONE) { - // TODO Animate this - mLowerContextView.setVisibility(View.GONE); - } + mContextView.closeMode(); mActionMode = null; + + if (mWasHiddenBeforeMode) { + hide(); + } } @Override public void invalidate() { - if (mCallback.onPrepareActionMode(this, mMenu)) { - // Refresh content in both context views + mMenu.stopDispatchingItemsChanged(); + try { + mCallback.onPrepareActionMode(this, mMenu); + } finally { + mMenu.startDispatchingItemsChanged(); + } + } + + public boolean dispatchOnCreate() { + mMenu.stopDispatchingItemsChanged(); + try { + return mCallback.onCreateActionMode(this, mMenu); + } finally { + mMenu.startDispatchingItemsChanged(); } } @Override public void setCustomView(View view) { - mUpperContextView.setCustomView(view); + mContextView.setCustomView(view); mCustomView = new WeakReference<View>(view); } @Override public void setSubtitle(CharSequence subtitle) { - mUpperContextView.setSubtitle(subtitle); + mContextView.setSubtitle(subtitle); } @Override public void setTitle(CharSequence title) { - mUpperContextView.setTitle(title); + mContextView.setTitle(title); } @Override @@ -673,12 +674,12 @@ public class ActionBarImpl extends ActionBar { @Override public CharSequence getTitle() { - return mUpperContextView.getTitle(); + return mContextView.getTitle(); } @Override public CharSequence getSubtitle() { - return mUpperContextView.getSubtitle(); + return mContextView.getSubtitle(); } @Override @@ -718,7 +719,7 @@ public class ActionBarImpl extends ActionBar { return; } invalidate(); - mUpperContextView.openOverflowMenu(); + mContextView.showOverflowMenu(); } } @@ -730,7 +731,7 @@ public class ActionBarImpl extends ActionBar { private Object mTag; private Drawable mIcon; private CharSequence mText; - private int mPosition; + private int mPosition = -1; private View mCustomView; @Override @@ -762,6 +763,9 @@ public class ActionBarImpl extends ActionBar { @Override public Tab setCustomView(View view) { mCustomView = view; + if (mPosition >= 0) { + mTabScrollView.updateTab(mPosition); + } return this; } @@ -792,6 +796,9 @@ public class ActionBarImpl extends ActionBar { @Override public Tab setIcon(Drawable icon) { mIcon = icon; + if (mPosition >= 0) { + mTabScrollView.updateTab(mPosition); + } return this; } @@ -803,6 +810,9 @@ public class ActionBarImpl extends ActionBar { @Override public Tab setText(CharSequence text) { mText = text; + if (mPosition >= 0) { + mTabScrollView.updateTab(mPosition); + } return this; } @@ -871,17 +881,25 @@ public class ActionBarImpl extends ActionBar { case NAVIGATION_MODE_TABS: mSavedTabPosition = getSelectedNavigationIndex(); selectTab(null); + if (!mActionView.hasEmbeddedTabs()) { + mTabScrollView.setVisibility(View.GONE); + } break; } mActionView.setNavigationMode(mode); switch (mode) { case NAVIGATION_MODE_TABS: + ensureTabsExist(); + if (!mActionView.hasEmbeddedTabs()) { + mTabScrollView.setVisibility(View.VISIBLE); + } if (mSavedTabPosition != INVALID_POSITION) { setSelectedNavigationItem(mSavedTabPosition); mSavedTabPosition = INVALID_POSITION; } break; } + mActionView.setCollapsable(mode == NAVIGATION_MODE_TABS && !mHasEmbeddedTabs); } @Override @@ -889,23 +907,24 @@ public class ActionBarImpl extends ActionBar { return mTabs.get(index); } - /** - * This fragment is added when we're keeping a back stack in a tab switch - * transaction. We use it to change the selected tab in the action bar view - * when we back out. - */ - private class SwitchSelectedTabViewFragment extends Fragment { - private int mSelectedTabIndex; - public SwitchSelectedTabViewFragment(int oldSelectedTab) { - mSelectedTabIndex = oldSelectedTab; - } + @Override + public void setIcon(int resId) { + mActionView.setIcon(resId); + } - @Override - public void onDetach() { - if (mSelectedTabIndex >= 0 && mSelectedTabIndex < getTabCount()) { - mActionView.setTabSelected(mSelectedTabIndex); - } - } + @Override + public void setIcon(Drawable icon) { + mActionView.setIcon(icon); + } + + @Override + public void setLogo(int resId) { + mActionView.setLogo(resId); + } + + @Override + public void setLogo(Drawable logo) { + mActionView.setLogo(logo); } } diff --git a/core/java/com/android/internal/app/IMediaContainerService.aidl b/core/java/com/android/internal/app/IMediaContainerService.aidl index aee1626..dd22e25 100755 --- a/core/java/com/android/internal/app/IMediaContainerService.aidl +++ b/core/java/com/android/internal/app/IMediaContainerService.aidl @@ -27,8 +27,9 @@ interface IMediaContainerService { String key, String resFileName); boolean copyResource(in Uri packageURI, in ParcelFileDescriptor outStream); - PackageInfoLite getMinimalPackageInfo(in Uri fileUri, int flags); - boolean checkFreeStorage(boolean external, in Uri fileUri); + PackageInfoLite getMinimalPackageInfo(in Uri fileUri, in int flags, in long threshold); + boolean checkInternalFreeStorage(in Uri fileUri, in long threshold); + boolean checkExternalFreeStorage(in Uri fileUri); ObbInfo getObbInfo(in String filename); long calculateDirectorySize(in String directory); } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 2e56996..ba2f5d4 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -30,7 +30,6 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.PatternMatcher; -import android.util.Config; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -238,7 +237,7 @@ public class ResolverActivity extends AlertActivity implements ResolveInfo r0 = rList.get(0); for (int i=1; i<N; i++) { ResolveInfo ri = rList.get(i); - if (Config.LOGV) Log.v( + if (false) Log.v( "ResolveListActivity", r0.activityInfo.name + "=" + r0.priority + "/" + r0.isDefault + " vs " + diff --git a/core/java/com/android/internal/content/NativeLibraryHelper.java b/core/java/com/android/internal/content/NativeLibraryHelper.java index 4ae55fc..9ae7def 100644 --- a/core/java/com/android/internal/content/NativeLibraryHelper.java +++ b/core/java/com/android/internal/content/NativeLibraryHelper.java @@ -4,7 +4,6 @@ import android.content.pm.PackageManager; import android.os.Build; import android.os.FileUtils; import android.os.SystemProperties; -import android.util.Config; import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -176,7 +175,7 @@ public class NativeLibraryHelper { continue; } - if (Config.LOGD) { + if (false) { Log.d(TAG, "Found gdbserver: " + entry.getName()); } diff --git a/core/java/com/android/internal/content/PackageHelper.java b/core/java/com/android/internal/content/PackageHelper.java index d6c43f9..b57046c 100644 --- a/core/java/com/android/internal/content/PackageHelper.java +++ b/core/java/com/android/internal/content/PackageHelper.java @@ -56,18 +56,13 @@ public class PackageHelper { return null; } - public static String createSdDir(long sizeBytes, String cid, + public static String createSdDir(int sizeMb, String cid, String sdEncKey, int uid) { // Create mount point via MountService IMountService mountService = getMountService(); - int sizeMb = (int) (sizeBytes >> 20); - if ((sizeBytes - (sizeMb * 1024 * 1024)) > 0) { - sizeMb++; - } - // Add buffer size - sizeMb++; + if (localLOGV) - Log.i(TAG, "Size of container " + sizeMb + " MB " + sizeBytes + " bytes"); + Log.i(TAG, "Size of container " + sizeMb + " MB"); try { int rc = mountService.createSecureContainer( diff --git a/core/java/com/android/internal/net/DomainNameValidator.java b/core/java/com/android/internal/net/DomainNameValidator.java index 36973f1..3950655 100644 --- a/core/java/com/android/internal/net/DomainNameValidator.java +++ b/core/java/com/android/internal/net/DomainNameValidator.java @@ -16,7 +16,6 @@ package com.android.internal.net; import android.net.NetworkUtils; -import android.util.Config; import android.util.Log; import java.net.InetAddress; @@ -35,7 +34,7 @@ public class DomainNameValidator { private final static String TAG = "DomainNameValidator"; private static final boolean DEBUG = false; - private static final boolean LOG_ENABLED = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean LOG_ENABLED = false; private static final int ALT_DNS_NAME = 2; private static final int ALT_IPA_NAME = 7; diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 2847cf3..12687a1 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -36,6 +36,7 @@ import android.telephony.ServiceState; import android.telephony.SignalStrength; import android.telephony.TelephonyManager; import android.util.Log; +import android.util.LogWriter; import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.Slog; @@ -70,7 +71,7 @@ public final class BatteryStatsImpl extends BatteryStats { private static final int MAGIC = 0xBA757475; // 'BATSTATS' // Current on-disk Parcel version - private static final int VERSION = 54; + private static final int VERSION = 60; // Maximum number of items we will record in the history. private static final int MAX_HISTORY_ITEMS = 2000; @@ -154,11 +155,27 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mHaveBatteryLevel = false; boolean mRecordingHistory = true; int mNumHistoryItems; + + static final int MAX_HISTORY_BUFFER = 128*1024; // 128KB + static final int MAX_MAX_HISTORY_BUFFER = 144*1024; // 144KB + final Parcel mHistoryBuffer = Parcel.obtain(); + final HistoryItem mHistoryLastWritten = new HistoryItem(); + final HistoryItem mHistoryLastLastWritten = new HistoryItem(); + final HistoryItem mHistoryReadTmp = new HistoryItem(); + int mHistoryBufferLastPos = -1; + boolean mHistoryOverflow = false; + long mLastHistoryTime = 0; + + final HistoryItem mHistoryCur = new HistoryItem(); + HistoryItem mHistory; HistoryItem mHistoryEnd; HistoryItem mHistoryLastEnd; HistoryItem mHistoryCache; - final HistoryItem mHistoryCur = new HistoryItem(); + + private HistoryItem mHistoryIterator; + private boolean mReadOverflow; + private boolean mIteratingHistory; int mStartCount; @@ -1189,9 +1206,84 @@ public final class BatteryStatsImpl extends BatteryStats { mBtHeadset = headset; } + int mChangedBufferStates = 0; + + void addHistoryBufferLocked(long curTime) { + if (!mHaveBatteryLevel || !mRecordingHistory) { + return; + } + + final long timeDiff = (mHistoryBaseTime+curTime) - mHistoryLastWritten.time; + if (mHistoryBufferLastPos >= 0 && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE + && timeDiff < 2000 + && ((mHistoryLastWritten.states^mHistoryCur.states)&mChangedBufferStates) == 0) { + // If the current is the same as the one before, then we no + // longer need the entry. + mHistoryBuffer.setDataSize(mHistoryBufferLastPos); + mHistoryBuffer.setDataPosition(mHistoryBufferLastPos); + mHistoryBufferLastPos = -1; + if (mHistoryLastLastWritten.cmd == HistoryItem.CMD_UPDATE + && timeDiff < 500 && mHistoryLastLastWritten.same(mHistoryCur)) { + // If this results in us returning to the state written + // prior to the last one, then we can just delete the last + // written one and drop the new one. Nothing more to do. + mHistoryLastWritten.setTo(mHistoryLastLastWritten); + mHistoryLastLastWritten.cmd = HistoryItem.CMD_NULL; + return; + } + mChangedBufferStates |= mHistoryLastWritten.states^mHistoryCur.states; + curTime = mHistoryLastWritten.time - mHistoryBaseTime; + mHistoryLastWritten.setTo(mHistoryLastLastWritten); + } else { + mChangedBufferStates = 0; + } + + final int dataSize = mHistoryBuffer.dataSize(); + if (dataSize >= MAX_HISTORY_BUFFER) { + if (!mHistoryOverflow) { + mHistoryOverflow = true; + addHistoryBufferLocked(curTime, HistoryItem.CMD_OVERFLOW); + } + + // Once we've reached the maximum number of items, we only + // record changes to the battery level and the most interesting states. + // Once we've reached the maximum maximum number of items, we only + // record changes to the battery level. + if (mHistoryLastWritten.batteryLevel == mHistoryCur.batteryLevel && + (dataSize >= MAX_MAX_HISTORY_BUFFER + || ((mHistoryEnd.states^mHistoryCur.states) + & HistoryItem.MOST_INTERESTING_STATES) == 0)) { + return; + } + } + + addHistoryBufferLocked(curTime, HistoryItem.CMD_UPDATE); + } + + void addHistoryBufferLocked(long curTime, byte cmd) { + int origPos = 0; + if (mIteratingHistory) { + origPos = mHistoryBuffer.dataPosition(); + mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); + } + mHistoryBufferLastPos = mHistoryBuffer.dataPosition(); + mHistoryLastLastWritten.setTo(mHistoryLastWritten); + mHistoryLastWritten.setTo(mHistoryBaseTime + curTime, cmd, mHistoryCur); + mHistoryLastWritten.writeDelta(mHistoryBuffer, mHistoryLastLastWritten); + mLastHistoryTime = curTime; + if (DEBUG_HISTORY) Slog.i(TAG, "Writing history buffer: was " + mHistoryBufferLastPos + + " now " + mHistoryBuffer.dataPosition() + + " size is now " + mHistoryBuffer.dataSize()); + if (mIteratingHistory) { + mHistoryBuffer.setDataPosition(origPos); + } + } + int mChangedStates = 0; void addHistoryRecordLocked(long curTime) { + addHistoryBufferLocked(curTime); + if (!mHaveBatteryLevel || !mRecordingHistory) { return; } @@ -1206,6 +1298,7 @@ public final class BatteryStatsImpl extends BatteryStats { // If the current is the same as the one before, then we no // longer need the entry. if (mHistoryLastEnd != null && mHistoryLastEnd.cmd == HistoryItem.CMD_UPDATE + && (mHistoryBaseTime+curTime) < (mHistoryEnd.time+500) && mHistoryLastEnd.same(mHistoryCur)) { mHistoryLastEnd.next = null; mHistoryEnd.next = mHistoryCache; @@ -1268,6 +1361,7 @@ public final class BatteryStatsImpl extends BatteryStats { } void clearHistoryLocked() { + if (DEBUG_HISTORY) Slog.i(TAG, "********** CLEARING HISTORY!"); if (mHistory != null) { mHistoryEnd.next = mHistoryCache; mHistoryCache = mHistory; @@ -1275,6 +1369,15 @@ public final class BatteryStatsImpl extends BatteryStats { } mNumHistoryItems = 0; mHistoryBaseTime = 0; + mLastHistoryTime = 0; + + mHistoryBuffer.setDataSize(0); + mHistoryBuffer.setDataPosition(0); + mHistoryBuffer.setDataCapacity(MAX_HISTORY_BUFFER/2); + mHistoryLastLastWritten.cmd = HistoryItem.CMD_NULL; + mHistoryLastWritten.cmd = HistoryItem.CMD_NULL; + mHistoryBufferLastPos = -1; + mHistoryOverflow = false; } public void doUnplugLocked(long batteryUptime, long batteryRealtime) { @@ -3910,11 +4013,13 @@ public final class BatteryStatsImpl extends BatteryStats { mDischargeUnplugLevel = 0; mDischargeCurrentLevel = 0; initDischarge(); + clearHistoryLocked(); } public BatteryStatsImpl(Parcel p) { mFile = null; mHandler = null; + clearHistoryLocked(); readFromParcel(p); } @@ -3932,25 +4037,84 @@ public final class BatteryStatsImpl extends BatteryStats { } } - private HistoryItem mHistoryIterator; - - public boolean startIteratingHistoryLocked() { + @Override + public boolean startIteratingOldHistoryLocked() { + if (DEBUG_HISTORY) Slog.i(TAG, "ITERATING: buff size=" + mHistoryBuffer.dataSize() + + " pos=" + mHistoryBuffer.dataPosition()); + mHistoryBuffer.setDataPosition(0); + mHistoryReadTmp.clear(); + mReadOverflow = false; + mIteratingHistory = true; return (mHistoryIterator = mHistory) != null; } - public boolean getNextHistoryLocked(HistoryItem out) { + @Override + public boolean getNextOldHistoryLocked(HistoryItem out) { + boolean end = mHistoryBuffer.dataPosition() >= mHistoryBuffer.dataSize(); + if (!end) { + mHistoryReadTmp.readDelta(mHistoryBuffer); + mReadOverflow |= mHistoryReadTmp.cmd == HistoryItem.CMD_OVERFLOW; + } HistoryItem cur = mHistoryIterator; if (cur == null) { + if (!mReadOverflow && !end) { + Slog.w(TAG, "Old history ends before new history!"); + } return false; } out.setTo(cur); mHistoryIterator = cur.next; + if (!mReadOverflow) { + if (end) { + Slog.w(TAG, "New history ends before old history!"); + } else if (!out.same(mHistoryReadTmp)) { + long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); + PrintWriter pw = new PrintWriter(new LogWriter(android.util.Log.WARN, TAG)); + pw.println("Histories differ!"); + pw.println("Old history:"); + (new HistoryPrinter()).printNextItem(pw, out, now); + pw.println("New history:"); + (new HistoryPrinter()).printNextItem(pw, mHistoryReadTmp, now); + } + } return true; } @Override - public HistoryItem getHistory() { - return mHistory; + public void finishIteratingOldHistoryLocked() { + mIteratingHistory = false; + mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); + } + + @Override + public boolean startIteratingHistoryLocked() { + if (DEBUG_HISTORY) Slog.i(TAG, "ITERATING: buff size=" + mHistoryBuffer.dataSize() + + " pos=" + mHistoryBuffer.dataPosition()); + mHistoryBuffer.setDataPosition(0); + mReadOverflow = false; + mIteratingHistory = true; + return mHistoryBuffer.dataSize() > 0; + } + + @Override + public boolean getNextHistoryLocked(HistoryItem out) { + final int pos = mHistoryBuffer.dataPosition(); + if (pos == 0) { + out.clear(); + } + boolean end = pos >= mHistoryBuffer.dataSize(); + if (end) { + return false; + } + + out.readDelta(mHistoryBuffer); + return true; + } + + @Override + public void finishIteratingHistoryLocked() { + mIteratingHistory = false; + mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); } @Override @@ -4697,7 +4861,9 @@ public final class BatteryStatsImpl extends BatteryStats { Slog.e("BatteryStats", "Error reading battery statistics", e); } - addHistoryRecordLocked(SystemClock.elapsedRealtime(), HistoryItem.CMD_START); + long now = SystemClock.elapsedRealtime(); + addHistoryRecordLocked(now, HistoryItem.CMD_START); + addHistoryBufferLocked(now, HistoryItem.CMD_START); } public int describeContents() { @@ -4705,30 +4871,54 @@ public final class BatteryStatsImpl extends BatteryStats { } void readHistory(Parcel in) { - mHistory = mHistoryEnd = mHistoryCache = null; - mHistoryBaseTime = 0; - long time; - while (in.dataAvail() > 0 && (time=in.readLong()) >= 0) { - HistoryItem rec = new HistoryItem(time, in); - addHistoryRecordLocked(rec); - if (rec.time > mHistoryBaseTime) { - mHistoryBaseTime = rec.time; - } + mHistoryBaseTime = in.readLong(); + + mHistoryBuffer.setDataSize(0); + mHistoryBuffer.setDataPosition(0); + + int bufSize = in.readInt(); + int curPos = in.dataPosition(); + if (bufSize >= (MAX_MAX_HISTORY_BUFFER*3)) { + Slog.w(TAG, "File corrupt: history data buffer too large " + bufSize); + } else if ((bufSize&~3) != bufSize) { + Slog.w(TAG, "File corrupt: history data buffer not aligned " + bufSize); + } else { + if (DEBUG_HISTORY) Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize + + " bytes at " + curPos); + mHistoryBuffer.appendFrom(in, curPos, bufSize); + in.setDataPosition(curPos + bufSize); } - long oldnow = SystemClock.elapsedRealtime() - (5*60*100); + long oldnow = SystemClock.elapsedRealtime() - (5*60*1000); if (oldnow > 0) { // If the system process has restarted, but not the entire // system, then the mHistoryBaseTime already accounts for // much of the elapsed time. We thus want to adjust it back, // to avoid large gaps in the data. We determine we are // in this case by arbitrarily saying it is so if at this - // point in boot the elapsed time is already more than 5 seconds. + // point in boot the elapsed time is already more than 5 minutes. mHistoryBaseTime -= oldnow; } } + void readOldHistory(Parcel in) { + mHistory = mHistoryEnd = mHistoryCache = null; + long time; + while (in.dataAvail() > 0 && (time=in.readLong()) >= 0) { + HistoryItem rec = new HistoryItem(time, in); + addHistoryRecordLocked(rec); + } + } + void writeHistory(Parcel out) { + out.writeLong(mLastHistoryTime); + out.writeInt(mHistoryBuffer.dataSize()); + if (DEBUG_HISTORY) Slog.i(TAG, "***************** WRITING HISTORY: " + + mHistoryBuffer.dataSize() + " bytes at " + out.dataPosition()); + out.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); + } + + void writeOldHistory(Parcel out) { HistoryItem rec = mHistory; while (rec != null) { if (rec.time >= 0) rec.writeToParcel(out, 0); @@ -4746,6 +4936,7 @@ public final class BatteryStatsImpl extends BatteryStats { } readHistory(in); + readOldHistory(in); mStartCount = in.readInt(); mBatteryUptime = in.readLong(); @@ -4935,6 +5126,9 @@ public final class BatteryStatsImpl extends BatteryStats { * @param out the Parcel to be written to. */ public void writeSummaryToParcel(Parcel out) { + // Need to update with current kernel wake lock counts. + updateKernelWakelocksLocked(); + final long NOW_SYS = SystemClock.uptimeMillis() * 1000; final long NOWREAL_SYS = SystemClock.elapsedRealtime() * 1000; final long NOW = getBatteryUptimeLocked(NOW_SYS); @@ -4943,6 +5137,7 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeInt(VERSION); writeHistory(out); + writeOldHistory(out); out.writeInt(mStartCount); out.writeLong(computeBatteryUptime(NOW_SYS, STATS_SINCE_CHARGED)); @@ -5256,6 +5451,9 @@ public final class BatteryStatsImpl extends BatteryStats { @SuppressWarnings("unused") void writeToParcelLocked(Parcel out, boolean inclUids, int flags) { + // Need to update with current kernel wake lock counts. + updateKernelWakelocksLocked(); + final long uSecUptime = SystemClock.uptimeMillis() * 1000; final long uSecRealtime = SystemClock.elapsedRealtime() * 1000; final long batteryUptime = getBatteryUptimeLocked(uSecUptime); @@ -5358,6 +5556,11 @@ public final class BatteryStatsImpl extends BatteryStats { } }; + public void prepareForDumpLocked() { + // Need to retrieve current kernel wake lock stats before printing. + updateKernelWakelocksLocked(); + } + public void dumpLocked(PrintWriter pw) { if (DEBUG) { Printer pr = new PrintWriterPrinter(pw); diff --git a/core/java/com/android/internal/os/BinderInternal.java b/core/java/com/android/internal/os/BinderInternal.java index ba0bf0d..f54a3e9 100644 --- a/core/java/com/android/internal/os/BinderInternal.java +++ b/core/java/com/android/internal/os/BinderInternal.java @@ -19,7 +19,6 @@ package com.android.internal.os; import android.os.Binder; import android.os.IBinder; import android.os.SystemClock; -import android.util.Config; import android.util.EventLog; import android.util.Log; diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java index f58f261..5e9cd23 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; @@ -33,7 +32,6 @@ import dalvik.system.VMRuntime; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.LogManager; import java.util.TimeZone; @@ -46,6 +44,7 @@ import org.apache.harmony.luni.internal.util.TimezoneGetter; */ public class RuntimeInit { private final static String TAG = "AndroidRuntime"; + private final static boolean DEBUG = false; /** true if commonInit() has been called */ private static boolean initialized; @@ -90,14 +89,14 @@ public class RuntimeInit { } private static final void commonInit() { - if (Config.LOGV) Slog.d(TAG, "Entered RuntimeInit!"); + if (DEBUG) 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 (DEBUG) Slog.d(TAG, ">>>>> qwerty keyboard = " + hasQwerty); if (hasQwerty == 1) { System.setProperty("qwerty", "1"); } @@ -184,11 +183,6 @@ public class RuntimeInit { */ private static void invokeStaticMain(String className, String[] argv) throws ZygoteInit.MethodAndArgsCaller { - - // We want to be fairly aggressive about heap utilization, to avoid - // holding on to a lot of memory that isn't needed. - VMRuntime.getRuntime().setTargetHeapUtilization(0.75f); - Class<?> cl; try { @@ -226,6 +220,13 @@ public class RuntimeInit { } public static final void main(String[] argv) { + if (argv.length == 2 && argv[1].equals("application")) { + if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application"); + redirectLogStreams(); + } else { + if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting tool"); + } + commonInit(); /* @@ -234,7 +235,7 @@ public class RuntimeInit { */ finishInit(); - if (Config.LOGV) Slog.d(TAG, "Leaving RuntimeInit!"); + if (DEBUG) Slog.d(TAG, "Leaving RuntimeInit!"); } public static final native void finishInit(); @@ -246,7 +247,6 @@ public class RuntimeInit { * * Current recognized args: * <ul> - * <li> --nice-name=<i>nice name to appear in ps</i> * <li> <code> [--] <start class name> <args> * </ul> * @@ -254,45 +254,60 @@ public class RuntimeInit { */ public static final void zygoteInit(String[] argv) throws ZygoteInit.MethodAndArgsCaller { - // TODO: Doing this here works, but it seems kind of arbitrary. Find - // a better place. The goal is to set it up for applications, but not - // tools like am. - System.out.close(); - System.setOut(new AndroidPrintStream(Log.INFO, "System.out")); - System.err.close(); - System.setErr(new AndroidPrintStream(Log.WARN, "System.err")); + if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application from zygote"); + + redirectLogStreams(); commonInit(); zygoteInitNative(); - int curArg = 0; - for ( /* curArg */ ; curArg < argv.length; curArg++) { - String arg = argv[curArg]; - - if (arg.equals("--")) { - curArg++; - break; - } else if (!arg.startsWith("--")) { - break; - } else if (arg.startsWith("--nice-name=")) { - String niceName = arg.substring(arg.indexOf('=') + 1); - Process.setArgV0(niceName); - } - } + applicationInit(argv); + } - if (curArg == argv.length) { - Slog.e(TAG, "Missing classname argument to RuntimeInit!"); + /** + * The main function called when an application is started through a + * wrapper process. + * + * When the wrapper starts, the runtime starts {@link RuntimeInit#main} + * which calls {@link WrapperInit#main} which then calls this method. + * So we don't need to call commonInit() here. + * + * @param argv arg strings + */ + public static void wrapperInit(String[] argv) + throws ZygoteInit.MethodAndArgsCaller { + if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application from wrapper"); + + applicationInit(argv); + } + + private static void applicationInit(String[] argv) + throws ZygoteInit.MethodAndArgsCaller { + // We want to be fairly aggressive about heap utilization, to avoid + // holding on to a lot of memory that isn't needed. + VMRuntime.getRuntime().setTargetHeapUtilization(0.75f); + + final Arguments args; + try { + args = new Arguments(argv); + } catch (IllegalArgumentException ex) { + Slog.e(TAG, ex.getMessage()); // let the process exit return; } // Remaining arguments are passed to the start class's static main + invokeStaticMain(args.startClass, args.startArgs); + } - String startClass = argv[curArg++]; - String[] startArgs = new String[argv.length - curArg]; - - System.arraycopy(argv, curArg, startArgs, 0, startArgs.length); - invokeStaticMain(startClass, startArgs); + /** + * Redirect System.out and System.err to the Android log. + */ + public static void redirectLogStreams() { + System.out.close(); + System.setOut(new AndroidPrintStream(Log.INFO, "System.out")); + System.err.close(); + System.setErr(new AndroidPrintStream(Log.WARN, "System.err")); } public static final native void zygoteInitNative(); @@ -352,4 +367,55 @@ public class RuntimeInit { // Register handlers for DDM messages. android.ddm.DdmRegister.registerHandlers(); } + + /** + * Handles argument parsing for args related to the runtime. + * + * Current recognized args: + * <ul> + * <li> <code> [--] <start class name> <args> + * </ul> + */ + static class Arguments { + /** first non-option argument */ + String startClass; + + /** all following arguments */ + String[] startArgs; + + /** + * Constructs instance and parses args + * @param args runtime command-line args + * @throws IllegalArgumentException + */ + Arguments(String args[]) throws IllegalArgumentException { + parseArgs(args); + } + + /** + * Parses the commandline arguments intended for the Runtime. + */ + private void parseArgs(String args[]) + throws IllegalArgumentException { + int curArg = 0; + for (; curArg < args.length; curArg++) { + String arg = args[curArg]; + + if (arg.equals("--")) { + curArg++; + break; + } else if (!arg.startsWith("--")) { + break; + } + } + + if (curArg == args.length) { + throw new IllegalArgumentException("Missing classname argument to RuntimeInit!"); + } + + startClass = args[curArg++]; + startArgs = new String[args.length - curArg]; + System.arraycopy(args, curArg, startArgs, 0, startArgs.length); + } + } } 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/WrapperInit.java b/core/java/com/android/internal/os/WrapperInit.java new file mode 100644 index 0000000..18d6caa --- /dev/null +++ b/core/java/com/android/internal/os/WrapperInit.java @@ -0,0 +1,117 @@ +/* + * 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.os; + +import android.os.Process; +import android.util.Slog; + +import java.io.DataOutputStream; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; + +import libcore.io.IoUtils; +import libcore.io.Libcore; + +import dalvik.system.Zygote; + +/** + * Startup class for the wrapper process. + * @hide + */ +public class WrapperInit { + private final static String TAG = "AndroidRuntime"; + + /** + * Class not instantiable. + */ + private WrapperInit() { + } + + /** + * The main function called when starting a runtime application through a + * wrapper process instead of by forking Zygote. + * + * The first argument specifies the file descriptor for a pipe that should receive + * the pid of this process, or 0 if none. The remaining arguments are passed to + * the runtime. + * + * @param args The command-line arguments. + */ + public static void main(String[] args) { + try { + int fdNum = Integer.parseInt(args[0], 10); + if (fdNum != 0) { + try { + FileDescriptor fd = ZygoteInit.createFileDescriptor(fdNum); + DataOutputStream os = new DataOutputStream(new FileOutputStream(fd)); + os.writeInt(Process.myPid()); + os.close(); + IoUtils.closeQuietly(fd); + } catch (IOException ex) { + Slog.d(TAG, "Could not write pid of wrapped process to Zygote pipe.", ex); + } + } + + String[] runtimeArgs = new String[args.length - 1]; + System.arraycopy(args, 1, runtimeArgs, 0, runtimeArgs.length); + RuntimeInit.wrapperInit(runtimeArgs); + } catch (ZygoteInit.MethodAndArgsCaller caller) { + caller.run(); + } + } + + /** + * Executes a runtime application with a wrapper command. + * This method never returns. + * + * @param invokeWith The wrapper command. + * @param niceName The nice name for the application, or null if none. + * @param pipeFd The pipe to which the application's pid should be written, or null if none. + * @param args Arguments for {@link RuntimeInit.main}. + */ + public static void execApplication(String invokeWith, String niceName, + FileDescriptor pipeFd, String[] args) { + StringBuilder command = new StringBuilder(invokeWith); + command.append(" /system/bin/app_process /system/bin --application"); + if (niceName != null) { + command.append(" '--nice-name=").append(niceName).append("'"); + } + command.append(" com.android.internal.os.WrapperInit "); + command.append(pipeFd != null ? pipeFd.getInt$() : 0); + Zygote.appendQuotedShellArgs(command, args); + Zygote.execShell(command.toString()); + } + + /** + * Executes a standalone application with a wrapper command. + * This method never returns. + * + * @param invokeWith The wrapper command. + * @param classPath The class path. + * @param className The class name to invoke. + * @param args Arguments for the main() method of the specified class. + */ + public static void execStandalone(String invokeWith, String classPath, String className, + String[] args) { + StringBuilder command = new StringBuilder(invokeWith); + command.append(" /system/bin/dalvikvm -classpath '").append(classPath); + command.append("' ").append(className); + Zygote.appendQuotedShellArgs(command, args); + Zygote.execShell(command.toString()); + } +} diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index c473fd2..b872e22 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -26,14 +26,20 @@ import dalvik.system.PathClassLoader; import dalvik.system.Zygote; import java.io.BufferedReader; +import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.FileDescriptor; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.util.ArrayList; +import libcore.io.ErrnoException; +import libcore.io.IoUtils; +import libcore.io.Libcore; + /** * A connection that can make spawn requests. */ @@ -193,15 +199,20 @@ class ZygoteConnection { new FileOutputStream(descriptors[2])); } - int pid; + int pid = -1; + FileDescriptor childPipeFd = null; + FileDescriptor serverPipeFd = null; try { parsedArgs = new Arguments(args); applyUidSecurityPolicy(parsedArgs, peer); - applyDebuggerSecurityPolicy(parsedArgs); applyRlimitSecurityPolicy(parsedArgs, peer); applyCapabilitiesSecurityPolicy(parsedArgs, peer); + applyInvokeWithSecurityPolicy(parsedArgs, peer); + + applyDebuggerSystemProperty(parsedArgs); + applyInvokeWithSystemProperty(parsedArgs); int[][] rlimits = null; @@ -209,25 +220,45 @@ class ZygoteConnection { rlimits = parsedArgs.rlimits.toArray(intArray2d); } + if (parsedArgs.runtimeInit && parsedArgs.invokeWith != null) { + FileDescriptor[] pipeFds = Libcore.os.pipe(); + childPipeFd = pipeFds[1]; + serverPipeFd = pipeFds[0]; + ZygoteInit.setCloseOnExec(serverPipeFd, true); + } + pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids, parsedArgs.debugFlags, rlimits); + } catch (IOException ex) { + logAndPrintError(newStderr, "Exception creating pipe", ex); + } catch (ErrnoException ex) { + logAndPrintError(newStderr, "Exception creating pipe", ex); } catch (IllegalArgumentException ex) { - logAndPrintError (newStderr, "Invalid zygote arguments", ex); - pid = -1; + logAndPrintError(newStderr, "Invalid zygote arguments", ex); } catch (ZygoteSecurityException ex) { logAndPrintError(newStderr, "Zygote security policy prevents request: ", ex); - pid = -1; } - if (pid == 0) { - // in child - handleChildProc(parsedArgs, descriptors, newStderr); - // should never happen - return true; - } else { /* pid != 0 */ - // in parent...pid of < 0 means failure - return handleParentProc(pid, descriptors, parsedArgs); + try { + if (pid == 0) { + // in child + IoUtils.closeQuietly(serverPipeFd); + serverPipeFd = null; + handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr); + + // should never get here, the child is expected to either + // throw ZygoteInit.MethodAndArgsCaller or exec(). + return true; + } else { + // in parent...pid of < 0 means failure + IoUtils.closeQuietly(childPipeFd); + childPipeFd = null; + return handleParentProc(pid, descriptors, serverPipeFd, parsedArgs); + } + } finally { + IoUtils.closeQuietly(childPipeFd); + IoUtils.closeQuietly(serverPipeFd); } } @@ -244,8 +275,8 @@ class ZygoteConnection { } /** - * Handles argument parsing for args related to the zygote spawner.<p> - + * Handles argument parsing for args related to the zygote spawner. + * * Current recognized args: * <ul> * <li> --setuid=<i>uid of child process, defaults to 0</i> @@ -274,6 +305,7 @@ class ZygoteConnection { * be handed off to com.android.internal.os.RuntimeInit, rather than * processed directly * Android runtime startup (eg, Binder initialization) is also eschewed. + * <li> --nice-name=<i>nice name to appear in ps</i> * <li> If <code>--runtime-init</code> is present: * [--] <args for RuntimeInit > * <li> If <code>--runtime-init</code> is absent: @@ -307,6 +339,9 @@ class ZygoteConnection { /** from --runtime-init */ boolean runtimeInit; + /** from --nice-name */ + String niceName; + /** from --capabilities */ boolean capabilitiesSpecified; long permittedCapabilities; @@ -315,6 +350,9 @@ class ZygoteConnection { /** from all --rlimit=r,c,m */ ArrayList<int[]> rlimits; + /** from --invoke-with */ + String invokeWith; + /** * Any args after and including the first non-option arg * (or after a '--') @@ -438,6 +476,23 @@ class ZygoteConnection { for (int i = params.length - 1; i >= 0 ; i--) { gids[i] = Integer.parseInt(params[i]); } + } else if (arg.equals("--invoke-with")) { + if (invokeWith != null) { + throw new IllegalArgumentException( + "Duplicate arg specified"); + } + try { + invokeWith = args[++curArg]; + } catch (IndexOutOfBoundsException ex) { + throw new IllegalArgumentException( + "--invoke-with requires argument"); + } + } else if (arg.startsWith("--nice-name=")) { + if (niceName != null) { + throw new IllegalArgumentException( + "Duplicate arg specified"); + } + niceName = arg.substring(arg.indexOf('=') + 1); } else { break; } @@ -567,14 +622,15 @@ class ZygoteConnection { /** - * Applies debugger security policy. + * Applies debugger system properties to the zygote arguments. + * * If "ro.debuggable" is "1", all apps are debuggable. Otherwise, * the debugger state is specified via the "--enable-debugger" flag * in the spawn request. * * @param args non-null; zygote spawner args */ - private static void applyDebuggerSecurityPolicy(Arguments args) { + public static void applyDebuggerSystemProperty(Arguments args) { if ("1".equals(SystemProperties.get("ro.debuggable"))) { args.debugFlags |= Zygote.DEBUG_ENABLE_DEBUGGER; } @@ -664,12 +720,56 @@ class ZygoteConnection { } /** + * Applies zygote security policy. + * Based on the credentials of the process issuing a zygote command: + * <ol> + * <li> uid 0 (root) may specify --invoke-with to launch Zygote with a + * wrapper command. + * <li> Any other uid may not specify any invoke-with argument. + * </ul> + * + * @param args non-null; zygote spawner arguments + * @param peer non-null; peer credentials + * @throws ZygoteSecurityException + */ + private static void applyInvokeWithSecurityPolicy(Arguments args, Credentials peer) + throws ZygoteSecurityException { + int peerUid = peer.getUid(); + + if (args.invokeWith != null && peerUid != 0) { + throw new ZygoteSecurityException("Peer is not permitted to specify " + + "an explicit invoke-with wrapper command"); + } + } + + /** + * Applies invoke-with system properties to the zygote arguments. + * + * @param parsedArgs non-null; zygote args + */ + public static void applyInvokeWithSystemProperty(Arguments args) { + if (args.invokeWith == null && args.niceName != null) { + if (args.niceName != null) { + String property = "wrap." + args.niceName; + if (property.length() > 31) { + property = property.substring(0, 31); + } + args.invokeWith = SystemProperties.get(property); + if (args.invokeWith != null && args.invokeWith.length() == 0) { + args.invokeWith = null; + } + } + } + } + + /** * Handles post-fork setup of child proc, closing sockets as appropriate, * reopen stdio as appropriate, and ultimately throwing MethodAndArgsCaller * if successful or returning if failed. * * @param parsedArgs non-null; zygote args * @param descriptors null-ok; new file descriptors for stdio if available. + * @param pipeFd null-ok; pipe for communication back to Zygote. * @param newStderr null-ok; stream to use for stderr until stdio * is reopened. * @@ -677,7 +777,7 @@ class ZygoteConnection { * trampoline to code that invokes static main. */ private void handleChildProc(Arguments parsedArgs, - FileDescriptor[] descriptors, PrintStream newStderr) + FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr) throws ZygoteInit.MethodAndArgsCaller { /* @@ -704,7 +804,7 @@ class ZygoteConnection { descriptors[1], descriptors[2]); for (FileDescriptor fd: descriptors) { - ZygoteInit.closeDescriptor(fd); + IoUtils.closeQuietly(fd); } newStderr = System.err; } catch (IOException ex) { @@ -712,37 +812,48 @@ class ZygoteConnection { } } - if (parsedArgs.runtimeInit) { - RuntimeInit.zygoteInit(parsedArgs.remainingArgs); - } else { - ClassLoader cloader; + if (parsedArgs.niceName != null) { + Process.setArgV0(parsedArgs.niceName); + } - if (parsedArgs.classpath != null) { - cloader - = new PathClassLoader(parsedArgs.classpath, - ClassLoader.getSystemClassLoader()); + if (parsedArgs.runtimeInit) { + if (parsedArgs.invokeWith != null) { + WrapperInit.execApplication(parsedArgs.invokeWith, + parsedArgs.niceName, pipeFd, parsedArgs.remainingArgs); } else { - cloader = ClassLoader.getSystemClassLoader(); + RuntimeInit.zygoteInit(parsedArgs.remainingArgs); } - + } else { String className; try { className = parsedArgs.remainingArgs[0]; } catch (ArrayIndexOutOfBoundsException ex) { - logAndPrintError (newStderr, + logAndPrintError(newStderr, "Missing required class name argument", null); return; } - String[] mainArgs - = new String[parsedArgs.remainingArgs.length - 1]; + String[] mainArgs = new String[parsedArgs.remainingArgs.length - 1]; System.arraycopy(parsedArgs.remainingArgs, 1, mainArgs, 0, mainArgs.length); - try { - ZygoteInit.invokeStaticMain(cloader, className, mainArgs); - } catch (RuntimeException ex) { - logAndPrintError (newStderr, "Error starting. ", ex); + if (parsedArgs.invokeWith != null) { + WrapperInit.execStandalone(parsedArgs.invokeWith, + parsedArgs.classpath, className, mainArgs); + } else { + ClassLoader cloader; + if (parsedArgs.classpath != null) { + cloader = new PathClassLoader(parsedArgs.classpath, + ClassLoader.getSystemClassLoader()); + } else { + cloader = ClassLoader.getSystemClassLoader(); + } + + try { + ZygoteInit.invokeStaticMain(cloader, className, mainArgs); + } catch (RuntimeException ex) { + logAndPrintError(newStderr, "Error starting.", ex); + } } } } @@ -754,36 +865,54 @@ class ZygoteConnection { * if < 0; * @param descriptors null-ok; file descriptors for child's new stdio if * specified. + * @param pipeFd null-ok; pipe for communication with child. * @param parsedArgs non-null; zygote args * @return true for "exit command loop" and false for "continue command * loop" */ private boolean handleParentProc(int pid, - FileDescriptor[] descriptors, Arguments parsedArgs) { + FileDescriptor[] descriptors, FileDescriptor pipeFd, Arguments parsedArgs) { + + if (pid > 0) { + setChildPgid(pid); + } + + if (descriptors != null) { + for (FileDescriptor fd: descriptors) { + IoUtils.closeQuietly(fd); + } + } - if(pid > 0) { - // Try to move the new child into the peer's process group. + if (pipeFd != null && pid > 0) { + DataInputStream is = new DataInputStream(new FileInputStream(pipeFd)); + int innerPid = -1; try { - ZygoteInit.setpgid(pid, ZygoteInit.getpgid(peer.getPid())); + innerPid = is.readInt(); } catch (IOException ex) { - // This exception is expected in the case where - // the peer is not in our session - // TODO get rid of this log message in the case where - // getsid(0) != getsid(peer.getPid()) - Log.i(TAG, "Zygote: setpgid failed. This is " - + "normal if peer is not in our session"); + Log.w(TAG, "Error reading pid from wrapped process, child may have died", ex); + } finally { + try { + is.close(); + } catch (IOException ex) { + } } - } - try { - if (descriptors != null) { - for (FileDescriptor fd: descriptors) { - ZygoteInit.closeDescriptor(fd); + // Ensure that the pid reported by the wrapped process is either the + // child process that we forked, or a descendant of it. + if (innerPid > 0) { + int parentPid = innerPid; + while (parentPid > 0 && parentPid != pid) { + parentPid = Process.getParentPid(parentPid); + } + if (parentPid > 0) { + Log.i(TAG, "Wrapped process has pid " + innerPid); + pid = innerPid; + } else { + Log.w(TAG, "Wrapped process reported a pid that is not a child of " + + "the process that we forked: childPid=" + pid + + " innerPid=" + innerPid); } } - } catch (IOException ex) { - Log.e(TAG, "Error closing passed descriptors in " - + "parent process", ex); } try { @@ -808,6 +937,20 @@ class ZygoteConnection { return false; } + private void setChildPgid(int pid) { + // Try to move the new child into the peer's process group. + try { + ZygoteInit.setpgid(pid, ZygoteInit.getpgid(peer.getPid())); + } catch (IOException ex) { + // This exception is expected in the case where + // the peer is not in our session + // TODO get rid of this log message in the case where + // getsid(0) != getsid(peer.getPid()) + Log.i(TAG, "Zygote: setpgid failed. This is " + + "normal if peer is not in our session"); + } + } + /** * Logs an error message and prints it to the specified stream, if * provided diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index dea53bf..157c0bf 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -23,15 +23,16 @@ import android.graphics.drawable.Drawable; import android.net.LocalServerSocket; import android.os.Debug; import android.os.FileUtils; +import android.os.Process; 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; @@ -68,7 +69,7 @@ public class ZygoteInit { private static final int PRELOAD_GC_THRESHOLD = 50000; public static final String USAGE_STRING = - " <\"true\"|\"false\" for startSystemServer>"; + " <\"start-system-server\"|\"\" for startSystemServer>"; private static LocalServerSocket sServerSocket; @@ -99,25 +100,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 +256,7 @@ public class ZygoteInit { runtime.setTargetHeapUtilization(0.8f); // Start with a clean slate. - runtime.gcSoftReferences(); + System.gc(); runtime.runFinalizationSync(); Debug.startAllocCounting(); @@ -292,16 +274,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 +307,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 +321,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 +332,7 @@ public class ZygoteInit { Debug.startAllocCounting(); try { - runtime.gcSoftReferences(); + System.gc(); runtime.runFinalizationSync(); mResources = Resources.getSystem(); mResources.startPreloading(); @@ -421,15 +365,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 +388,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 +422,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(); } @@ -498,11 +442,20 @@ public class ZygoteInit { // set umask to 0077 so new files and directories will default to owner-only permissions. FileUtils.setUMask(FileUtils.S_IRWXG | FileUtils.S_IRWXO); - /* - * Pass the remaining arguments to SystemServer. - * "--nice-name=system_server com.android.server.SystemServer" - */ - RuntimeInit.zygoteInit(parsedArgs.remainingArgs); + if (parsedArgs.niceName != null) { + Process.setArgV0(parsedArgs.niceName); + } + + if (parsedArgs.invokeWith != null) { + WrapperInit.execApplication(parsedArgs.invokeWith, + parsedArgs.niceName, null, parsedArgs.remainingArgs); + } else { + /* + * Pass the remaining arguments to SystemServer. + */ + RuntimeInit.zygoteInit(parsedArgs.remainingArgs); + } + /* should never reach here */ } @@ -527,20 +480,13 @@ public class ZygoteInit { try { parsedArgs = new ZygoteConnection.Arguments(args); - - /* - * Enable debugging of the system process if *either* the command line flags - * indicate it should be debuggable or the ro.debuggable system property - * is set to "1" - */ - int debugFlags = parsedArgs.debugFlags; - if ("1".equals(SystemProperties.get("ro.debuggable"))) - debugFlags |= Zygote.DEBUG_ENABLE_DEBUGGER; + ZygoteConnection.applyDebuggerSystemProperty(parsedArgs); + ZygoteConnection.applyInvokeWithSystemProperty(parsedArgs); /* Request to fork the system server process */ pid = Zygote.forkSystemServer( parsedArgs.uid, parsedArgs.gid, - parsedArgs.gids, debugFlags, null, + parsedArgs.gids, parsedArgs.debugFlags, null, parsedArgs.permittedCapabilities, parsedArgs.effectiveCapabilities); } catch (IllegalArgumentException ex) { @@ -564,7 +510,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()); @@ -580,9 +525,9 @@ public class ZygoteInit { throw new RuntimeException(argv[0] + USAGE_STRING); } - if (argv[1].equals("true")) { + if (argv[1].equals("start-system-server")) { startSystemServer(); - } else if (!argv[1].equals("false")) { + } else if (!argv[1].equals("")) { throw new RuntimeException(argv[0] + USAGE_STRING); } @@ -754,15 +699,6 @@ public class ZygoteInit { FileDescriptor out, FileDescriptor err) throws IOException; /** - * Calls close() on a file descriptor - * - * @param fd descriptor to close - * @throws IOException - */ - static native void closeDescriptor(FileDescriptor fd) - throws IOException; - - /** * Toggles the close-on-exec flag for the specified file descriptor. * * @param fd non-null; file descriptor diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 7f23ed5..7d21489 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -34,5 +34,6 @@ oneway interface IStatusBar void setMenuKeyVisible(boolean visible); void setImeWindowStatus(in IBinder token, int vis, int backDisposition); void setHardKeyboardStatus(boolean available, boolean enabled); + void userActivity(); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index d6ca426..bfc717b 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -46,4 +46,5 @@ interface IStatusBarService void onNotificationClear(String pkg, String tag, int id); void setSystemUiVisibility(int vis); void setHardKeyboardEnabled(boolean enabled); + void userActivity(); } diff --git a/core/java/com/android/internal/util/Objects.java b/core/java/com/android/internal/util/Objects.java index 598a079..2664182 100644 --- a/core/java/com/android/internal/util/Objects.java +++ b/core/java/com/android/internal/util/Objects.java @@ -16,34 +16,47 @@ package com.android.internal.util; +import java.util.Arrays; + /** * Object utility methods. */ public class Objects { /** - * Ensures the given object isn't {@code null}. + * Determines whether two possibly-null objects are equal. Returns: + * + * <ul> + * <li>{@code true} if {@code a} and {@code b} are both null. + * <li>{@code true} if {@code a} and {@code b} are both non-null and they are + * equal according to {@link Object#equals(Object)}. + * <li>{@code false} in all other situations. + * </ul> * - * @return the given object - * @throws NullPointerException if the object is null + * <p>This assumes that any non-null objects passed to this function conform + * to the {@code equals()} contract. */ - public static <T> T nonNull(T t) { - if (t == null) { - throw new NullPointerException(); - } - return t; + public static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); } /** - * Ensures the given object isn't {@code null}. + * Generates a hash code for multiple values. The hash code is generated by + * calling {@link Arrays#hashCode(Object[])}. * - * @return the given object - * @throws NullPointerException if the object is null + * <p>This is useful for implementing {@link Object#hashCode()}. For example, + * in an object that has three properties, {@code x}, {@code y}, and + * {@code z}, one could write: + * <pre> + * public int hashCode() { + * return Objects.hashCode(getX(), getY(), getZ()); + * }</pre> + * + * <b>Warning</b>: When a single object is supplied, the returned hash code + * does not equal the hash code of that object. */ - public static <T> T nonNull(T t, String message) { - if (t == null) { - throw new NullPointerException(message); - } - return t; + public static int hashCode(Object... objects) { + return Arrays.hashCode(objects); } + } diff --git a/core/java/com/android/internal/util/Preconditions.java b/core/java/com/android/internal/util/Preconditions.java new file mode 100644 index 0000000..a53a9c0 --- /dev/null +++ b/core/java/com/android/internal/util/Preconditions.java @@ -0,0 +1,57 @@ +/* + * 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.util; + +/** + * Simple static methods to be called at the start of your own methods to verify + * correct arguments and state. + */ +public class Preconditions { + + /** + * Ensures that an object reference passed as a parameter to the calling + * method is not null. + * + * @param reference an object reference + * @return the non-null reference that was validated + * @throws NullPointerException if {@code reference} is null + */ + public static <T> T checkNotNull(T reference) { + if (reference == null) { + throw new NullPointerException(); + } + return reference; + } + + /** + * Ensures that an object reference passed as a parameter to the calling + * method is not null. + * + * @param reference an object reference + * @param errorMessage the exception message to use if the check fails; will + * be converted to a string using {@link String#valueOf(Object)} + * @return the non-null reference that was validated + * @throws NullPointerException if {@code reference} is null + */ + public static <T> T checkNotNull(T reference, Object errorMessage) { + if (reference == null) { + throw new NullPointerException(String.valueOf(errorMessage)); + } + return reference; + } + +} diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index c41b2cb..b9948fe 100644 --- a/core/java/com/android/internal/view/BaseIWindow.java +++ b/core/java/com/android/internal/view/BaseIWindow.java @@ -16,8 +16,6 @@ package com.android.internal.view; -import android.content.ClipData; -import android.content.ClipDescription; import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; @@ -26,8 +24,6 @@ import android.os.RemoteException; import android.view.DragEvent; import android.view.IWindow; import android.view.IWindowSession; -import android.view.KeyEvent; -import android.view.MotionEvent; public class BaseIWindow extends IWindow.Stub { private IWindowSession mSession; 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..812f92b 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -17,6 +17,7 @@ package com.android.internal.view; import android.os.ResultReceiver; +import android.text.style.SuggestionSpan; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.EditorInfo; @@ -33,6 +34,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(); @@ -60,8 +62,11 @@ interface IInputMethodManager { void showMySoftInput(in IBinder token, int flags); void updateStatusIcon(in IBinder token, String packageName, int iconId); void setImeWindowStatus(in IBinder token, int vis, int backDisposition); + void registerSuggestionSpansForNotification(in SuggestionSpan[] spans); + boolean notifySuggestionPicked(in SuggestionSpan span, String originalString, int index); InputMethodSubtype getCurrentInputMethodSubtype(); boolean setCurrentInputMethodSubtype(in InputMethodSubtype subtype); boolean switchToLastInputMethod(in IBinder token); boolean setInputMethodEnabled(String id, boolean enabled); + boolean setAdditionalInputMethodSubtypes(in IBinder token, in InputMethodSubtype[] subtypes); } 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/ActionMenuItem.java b/core/java/com/android/internal/view/menu/ActionMenuItem.java index 0ef4861..a4bcf60 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuItem.java +++ b/core/java/com/android/internal/view/menu/ActionMenuItem.java @@ -236,4 +236,31 @@ public class ActionMenuItem implements MenuItem { public MenuItem setActionView(int resId) { throw new UnsupportedOperationException(); } + + @Override + public MenuItem setShowAsActionFlags(int actionEnum) { + setShowAsAction(actionEnum); + return this; + } + + @Override + public boolean expandActionView() { + return false; + } + + @Override + public boolean collapseActionView() { + return false; + } + + @Override + public boolean isActionViewExpanded() { + return false; + } + + @Override + public MenuItem setOnActionExpandListener(OnActionExpandListener listener) { + // No need to save the listener; ActionMenuItem does not support collapsing items. + return this; + } } diff --git a/core/java/com/android/internal/view/menu/ActionMenuItemView.java b/core/java/com/android/internal/view/menu/ActionMenuItemView.java index 3325df6..479788d 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuItemView.java +++ b/core/java/com/android/internal/view/menu/ActionMenuItemView.java @@ -18,6 +18,7 @@ package com.android.internal.view.menu; import android.content.Context; import android.graphics.drawable.Drawable; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.Button; @@ -28,7 +29,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 +57,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() { @@ -102,6 +104,12 @@ public class ActionMenuItemView extends LinearLayout // TODO Support checkable action items } + private void updateTextButtonVisibility() { + boolean visible = !TextUtils.isEmpty(mTextButton.getText()); + visible = visible && (mImageButton.getDrawable() == null || mItemData.showsTextAsAction()); + mTextButton.setVisibility(visible ? VISIBLE : GONE); + } + public void setIcon(Drawable icon) { mImageButton.setImageDrawable(icon); if (icon != null) { @@ -110,9 +118,9 @@ public class ActionMenuItemView extends LinearLayout mImageButton.setVisibility(GONE); } - mTextButton.setVisibility(icon == null || mItemData.showsTextAsAction() ? VISIBLE : GONE); + updateTextButtonVisibility(); } - + public boolean hasText() { return mTextButton.getVisibility() != GONE; } @@ -127,13 +135,20 @@ public class ActionMenuItemView extends LinearLayout // populate accessibility description with title setContentDescription(title); - if (mImageButton.getDrawable() == null || mItemData.showsTextAsAction()) { - mTextButton.setText(mTitle); - mTextButton.setVisibility(VISIBLE); - } + mTextButton.setText(mTitle); + + updateTextButtonVisibility(); } 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..98c2747 --- /dev/null +++ b/core/java/com/android/internal/view/menu/ActionMenuPresenter.java @@ -0,0 +1,476 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.view.menu; + +import com.android.internal.view.menu.ActionMenuView.ActionMenuChildView; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.util.SparseBooleanArray; +import android.view.MenuItem; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.widget.ImageButton; + +import java.util.ArrayList; + +/** + * MenuPresenter for building action menus as seen in the action bar and action modes. + */ +public class ActionMenuPresenter extends BaseMenuPresenter { + private static final String TAG = "ActionMenuPresenter"; + + private View mOverflowButton; + private boolean mReserveOverflow; + private boolean mReserveOverflowSet; + private int mWidthLimit; + private int mActionItemWidthLimit; + private int mMaxItems; + private boolean mMaxItemsSet; + private boolean mStrictWidthLimit; + private boolean mWidthLimitSet; + + // 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(); + + if (!mReserveOverflowSet) { + // TODO Use the no-buttons specifier instead here + mReserveOverflow = res.getConfiguration() + .isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); + } + + if (!mWidthLimitSet) { + mWidthLimit = res.getDisplayMetrics().widthPixels / 2; + } + + // Measure for initial configuration + if (!mMaxItemsSet) { + mMaxItems = res.getInteger(com.android.internal.R.integer.max_action_buttons); + } + + int width = mWidthLimit; + if (mReserveOverflow) { + if (mOverflowButton == null) { + mOverflowButton = new OverflowMenuButton(mContext); + final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + mOverflowButton.measure(spec, spec); + } + width -= mOverflowButton.getMeasuredWidth(); + } else { + mOverflowButton = null; + } + + mActionItemWidthLimit = width; + + // Drop a scrap view as it may no longer reflect the proper context/config. + mScrapActionButtonView = null; + } + + public void setWidthLimit(int width, boolean strict) { + mWidthLimit = width; + mStrictWidthLimit = strict; + mWidthLimitSet = true; + } + + public void setReserveOverflow(boolean reserveOverflow) { + mReserveOverflow = reserveOverflow; + mReserveOverflowSet = true; + } + + public void setItemLimit(int itemCount) { + mMaxItems = itemCount; + mMaxItemsSet = true; + } + + @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) { + View actionView = item.getActionView(); + actionView = actionView != null && !item.hasCollapsibleActionView() ? + actionView : super.getItemView(item, convertView, parent); + actionView.setVisibility(item.isActionViewExpanded() ? View.GONE : View.VISIBLE); + return actionView; + } + + @Override + public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) { + itemView.initialize(item, 0); + ((ActionMenuItemView) itemView).setItemInvoker((ActionMenuView) mMenuView); + } + + @Override + public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) { + return item.isActionButton(); + } + + @Override + public void updateMenuView(boolean cleared) { + super.updateMenuView(cleared); + + if (mReserveOverflow && mMenu.getNonActionItems().size() > 0) { + if (mOverflowButton == null) { + mOverflowButton = new OverflowMenuButton(mContext); + mOverflowButton.setLayoutParams( + ((ActionMenuView) mMenuView).generateOverflowButtonLayoutParams()); + } + ViewGroup parent = (ViewGroup) mOverflowButton.getParent(); + if (parent != mMenuView) { + if (parent != null) { + parent.removeView(mOverflowButton); + } + ((ViewGroup) mMenuView).addView(mOverflowButton); + } + } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) { + ((ViewGroup) mMenuView).removeView(mOverflowButton); + } + } + + @Override + public boolean filterLeftoverView(ViewGroup parent, int childIndex) { + if (parent.getChildAt(childIndex) == mOverflowButton) return false; + return super.filterLeftoverView(parent, childIndex); + } + + public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + if (!subMenu.hasVisibleItems()) return false; + + SubMenuBuilder topSubMenu = subMenu; + while (topSubMenu.getParentMenu() != mMenu) { + topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu(); + } + View anchor = findViewForItem(topSubMenu.getItem()); + if (anchor == null) return false; + + mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu); + mActionButtonPopup.setAnchorView(anchor); + mActionButtonPopup.show(); + super.onSubMenuSelected(subMenu); + return true; + } + + private View findViewForItem(MenuItem item) { + final ViewGroup parent = (ViewGroup) mMenuView; + if (parent == null) return null; + + final int count = parent.getChildCount(); + for (int i = 0; i < count; i++) { + final View child = parent.getChildAt(i); + if (child instanceof MenuView.ItemView && + ((MenuView.ItemView) child).getItemData() == item) { + return child; + } + } + return null; + } + + /** + * Display the overflow menu if one is present. + * @return true if the overflow menu was shown, false otherwise. + */ + public boolean showOverflowMenu() { + if (mReserveOverflow && !isOverflowMenuShowing() && mMenuView != null && + mPostedOpenRunnable == null) { + OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true); + mPostedOpenRunnable = new OpenOverflowRunnable(popup); + // Post this for later; we might still need a layout for the anchor to be right. + ((View) mMenuView).post(mPostedOpenRunnable); + + // ActionMenuPresenter uses null as a callback argument here + // to indicate overflow is opening. + super.onSubMenuSelected(null); + + return true; + } + return false; + } + + /** + * Hide the overflow menu if it is currently showing. + * + * @return true if the overflow menu was hidden, false otherwise. + */ + public boolean hideOverflowMenu() { + if (mPostedOpenRunnable != null && mMenuView != null) { + ((View) mMenuView).removeCallbacks(mPostedOpenRunnable); + return true; + } + + MenuPopupHelper popup = mOverflowPopup; + if (popup != null) { + popup.dismiss(); + return true; + } + return false; + } + + /** + * Dismiss all popup menus - overflow and submenus. + * @return true if popups were dismissed, false otherwise. (This can be because none were open.) + */ + public boolean dismissPopupMenus() { + boolean result = hideOverflowMenu(); + result |= hideSubMenus(); + return result; + } + + /** + * Dismiss all submenu popups. + * + * @return true if popups were dismissed, false otherwise. (This can be because none were open.) + */ + public boolean hideSubMenus() { + if (mActionButtonPopup != null) { + mActionButtonPopup.dismiss(); + return true; + } + return false; + } + + /** + * @return true if the overflow menu is currently showing + */ + public boolean isOverflowMenuShowing() { + return mOverflowPopup != null && mOverflowPopup.isShowing(); + } + + /** + * @return true if space has been reserved in the action menu for an overflow item. + */ + public boolean isOverflowReserved() { + return mReserveOverflow; + } + + public boolean flagActionItems() { + final ArrayList<MenuItemImpl> visibleItems = mMenu.getVisibleItems(); + final int itemsSize = visibleItems.size(); + int maxActions = mMaxItems; + int widthLimit = mActionItemWidthLimit; + final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + final ViewGroup parent = (ViewGroup) mMenuView; + + int requiredItems = 0; + int requestedItems = 0; + int firstActionWidth = 0; + boolean hasOverflow = false; + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + if (item.requiresActionButton()) { + requiredItems++; + } else if (item.requestsActionButton()) { + requestedItems++; + } else { + hasOverflow = true; + } + } + + // Reserve a spot for the overflow item if needed. + if (mReserveOverflow && + (hasOverflow || requiredItems + requestedItems > maxActions)) { + maxActions--; + } + maxActions -= requiredItems; + + final SparseBooleanArray seenGroups = mActionButtonGroups; + seenGroups.clear(); + + // Flag as many more requested items as will fit. + for (int i = 0; i < itemsSize; i++) { + MenuItemImpl item = visibleItems.get(i); + + if (item.requiresActionButton()) { + View v = item.getActionView(); + if (v == null || item.hasCollapsibleActionView()) { + 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 || item.hasCollapsibleActionView()) { + v = getItemView(item, mScrapActionButtonView, parent); + if (mScrapActionButtonView == null) { + mScrapActionButtonView = v; + } + } + v.measure(querySpec, querySpec); + final int measuredWidth = v.getMeasuredWidth(); + widthLimit -= measuredWidth; + if (firstActionWidth == 0) { + firstActionWidth = measuredWidth; + } + + if (mStrictWidthLimit) { + isAction = widthLimit >= 0; + } else { + // Did this push the entire first item past the limit? + isAction = widthLimit + firstActionWidth > 0; + } + } + + if (isAction && groupId != 0) { + seenGroups.put(groupId, true); + } else if (inGroup) { + // We broke the width limit. Demote the whole group, they all overflow now. + seenGroups.put(groupId, false); + for (int j = 0; j < i; j++) { + MenuItemImpl areYouMyGroupie = visibleItems.get(j); + if (areYouMyGroupie.getGroupId() == groupId) { + areYouMyGroupie.setIsActionButton(false); + } + } + } + + item.setIsActionButton(isAction); + } + } + return true; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + dismissPopupMenus(); + super.onCloseMenu(menu, allMenusAreClosing); + } + + private class OverflowMenuButton extends ImageButton implements ActionMenuChildView { + public OverflowMenuButton(Context context) { + super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle); + + setClickable(true); + setFocusable(true); + setVisibility(VISIBLE); + setEnabled(true); + } + + @Override + public boolean performClick() { + if (super.performClick()) { + return true; + } + + playSoundEffect(SoundEffectConstants.CLICK); + showOverflowMenu(); + return true; + } + + public boolean needsDividerBefore() { + return true; + } + + public boolean needsDividerAfter() { + return false; + } + } + + private class OverflowPopup extends MenuPopupHelper { + public OverflowPopup(Context context, MenuBuilder menu, View anchorView, + boolean overflowOnly) { + super(context, menu, anchorView, overflowOnly); + } + + @Override + public void onDismiss() { + super.onDismiss(); + mMenu.close(); + mOverflowPopup = null; + } + } + + private class ActionButtonSubmenu extends MenuPopupHelper { + private SubMenuBuilder mSubMenu; + + public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu) { + super(context, subMenu); + mSubMenu = subMenu; + + MenuItemImpl item = (MenuItemImpl) subMenu.getItem(); + if (!item.isActionButton()) { + // Give a reasonable anchor to nested submenus. + setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton); + } + } + + @Override + public void onDismiss() { + super.onDismiss(); + mSubMenu.close(); + mActionButtonPopup = null; + } + } + + private class OpenOverflowRunnable implements Runnable { + private OverflowPopup mPopup; + + public OpenOverflowRunnable(OverflowPopup popup) { + mPopup = popup; + } + + public void run() { + mMenu.changeMenuMode(); + if (mPopup.tryShow()) { + mOverflowPopup = mPopup; + mPostedOpenRunnable = null; + } + } + } +} diff --git a/core/java/com/android/internal/view/menu/ActionMenuView.java b/core/java/com/android/internal/view/menu/ActionMenuView.java index 5da5e44..7b4f216 100644 --- a/core/java/com/android/internal/view/menu/ActionMenuView.java +++ b/core/java/com/android/internal/view/menu/ActionMenuView.java @@ -17,63 +17,25 @@ package com.android.internal.view.menu; import android.content.Context; import android.content.res.Configuration; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.Gravity; -import android.view.SoundEffectConstants; import android.view.View; +import android.view.ViewDebug; import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; import android.widget.LinearLayout; -import java.util.ArrayList; - /** * @hide */ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView { private static final String TAG = "ActionMenuView"; - - // TODO Theme/style this. - private static final int DIVIDER_PADDING = 12; // dips private MenuBuilder mMenu; - private int mMaxItems; - private int mWidthLimit; private boolean mReserveOverflow; - private OverflowMenuButton mOverflowButton; - private MenuPopupHelper mOverflowPopup; - - private float mDividerPadding; - - private Drawable mDivider; - - private final Runnable mShowOverflow = new Runnable() { - public void run() { - showOverflowMenu(); - } - }; - - private class OpenOverflowRunnable implements Runnable { - private MenuPopupHelper mPopup; - - public OpenOverflowRunnable(MenuPopupHelper popup) { - mPopup = popup; - } - - public void run() { - if (mPopup.tryShow()) { - mOverflowPopup = mPopup; - mPostedOpenRunnable = null; - } - } - } - - private OpenOverflowRunnable mPostedOpenRunnable; + private ActionMenuPresenter mPresenter; + private boolean mUpdateContentsBeforeMeasure; + private boolean mFormatItems; public ActionMenuView(Context context) { this(context, null); @@ -81,57 +43,112 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo public ActionMenuView(Context context, AttributeSet attrs) { super(context, attrs); - - final Resources res = getResources(); - - // Measure for initial configuration - mMaxItems = getMaxActionButtons(); - - // TODO There has to be a better way to indicate that we don't have a hard menu key. - final Configuration config = res.getConfiguration(); - mReserveOverflow = config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); - mWidthLimit = res.getDisplayMetrics().widthPixels / 2; - - TypedArray a = context.obtainStyledAttributes(com.android.internal.R.styleable.Theme); - mDivider = a.getDrawable(com.android.internal.R.styleable.Theme_dividerVertical); - a.recycle(); - - mDividerPadding = DIVIDER_PADDING * res.getDisplayMetrics().density; - setBaselineAligned(false); } + public void setPresenter(ActionMenuPresenter presenter) { + mPresenter = presenter; + } + @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - mReserveOverflow = newConfig.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE); - mMaxItems = getMaxActionButtons(); - mWidthLimit = getResources().getDisplayMetrics().widthPixels / 2; - if (mMenu != null) { - mMenu.setMaxActionItems(mMaxItems); - updateChildren(false); + mPresenter.updateMenuView(false); + + if (mPresenter != null && mPresenter.isOverflowMenuShowing()) { + mPresenter.hideOverflowMenu(); + mPresenter.showOverflowMenu(); } + } - if (mOverflowPopup != null && mOverflowPopup.isShowing()) { - mOverflowPopup.dismiss(); - post(mShowOverflow); + @Override + public void requestLayout() { + // Layout can influence how many action items fit. + mUpdateContentsBeforeMeasure = true; + super.requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mUpdateContentsBeforeMeasure && mMenu != null) { + mMenu.onItemsChanged(true); + mUpdateContentsBeforeMeasure = false; } + // If we've been given an exact size to match, apply special formatting during layout. + mFormatItems = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - if (mOverflowPopup != null && mOverflowPopup.isShowing()) { - mOverflowPopup.dismiss(); + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (!mFormatItems) { + super.onLayout(changed, left, top, right, bottom); + return; + } + + final int childCount = getChildCount(); + final int midVertical = (top + bottom) / 2; + final int dividerWidth = getDividerWidth(); + int overflowWidth = 0; + int nonOverflowWidth = 0; + int nonOverflowCount = 0; + int widthRemaining = right - left - getPaddingRight() - getPaddingLeft(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + if (v.getVisibility() == GONE) { + continue; + } + + LayoutParams p = (LayoutParams) v.getLayoutParams(); + if (p.isOverflowButton) { + overflowWidth = v.getMeasuredWidth(); + if (hasDividerBeforeChildAt(i)) { + overflowWidth += dividerWidth; + } + + int height = v.getMeasuredHeight(); + int r = getPaddingRight(); + int l = r - overflowWidth; + int t = midVertical - (height / 2); + int b = t + height; + v.layout(l, t, r, b); + + widthRemaining -= overflowWidth; + } else { + nonOverflowWidth += v.getMeasuredWidth() + p.leftMargin + p.rightMargin; + if (hasDividerBeforeChildAt(i)) { + nonOverflowWidth += dividerWidth; + } + nonOverflowCount++; + } } - removeCallbacks(mShowOverflow); - if (mPostedOpenRunnable != null) { - removeCallbacks(mPostedOpenRunnable); + + // Fill action items from the left. Overflow will always pin to the right edge. + if (nonOverflowWidth <= widthRemaining - overflowWidth) { + widthRemaining -= overflowWidth; + } + + int startLeft = getPaddingLeft(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + final LayoutParams lp = (LayoutParams) v.getLayoutParams(); + if (v.getVisibility() == GONE || lp.isOverflowButton) { + continue; + } + + startLeft += lp.leftMargin; + int width = v.getMeasuredWidth(); + int height = v.getMeasuredHeight(); + int t = midVertical - (height / 2); + v.layout(startLeft, t, startLeft + width, t + height); + startLeft += width + lp.rightMargin; } } - private int getMaxActionButtons() { - return getResources().getInteger(com.android.internal.R.integer.max_action_buttons); + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mPresenter.dismissPopupMenus(); } public boolean isOverflowReserved() { @@ -141,10 +158,6 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo public void setOverflowReserved(boolean reserveOverflow) { mReserveOverflow = reserveOverflow; } - - public View getOverflowButton() { - return mOverflowButton; - } @Override protected LayoutParams generateDefaultLayoutParams() { @@ -166,6 +179,17 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo return generateDefaultLayoutParams(); } + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams; + } + + public LayoutParams generateOverflowButtonLayoutParams() { + LayoutParams result = generateDefaultLayoutParams(); + result.isOverflowButton = true; + return result; + } + public boolean invokeItem(MenuItemImpl item) { return mMenu.performItemAction(item, 0); } @@ -174,243 +198,50 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo return 0; } - public void initialize(MenuBuilder menu, int menuType) { - int width = mWidthLimit; - if (mReserveOverflow) { - if (mOverflowButton == null) { - OverflowMenuButton button = new OverflowMenuButton(mContext); - mOverflowButton = button; - } - final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - mOverflowButton.measure(spec, spec); - width -= mOverflowButton.getMeasuredWidth(); - } - - menu.setActionWidthLimit(width); - - menu.setMaxActionItems(mMaxItems); - final boolean cleared = mMenu != menu; + public void initialize(MenuBuilder menu) { mMenu = menu; - updateChildren(cleared); } - public void updateChildren(boolean cleared) { - final ArrayList<MenuItemImpl> itemsToShow = mMenu.getActionItems(mReserveOverflow); - final int itemCount = itemsToShow.size(); - - boolean needsDivider = false; - int childIndex = 0; - for (int i = 0; i < itemCount; i++) { - final MenuItemImpl itemData = itemsToShow.get(i); - boolean hasDivider = false; - - if (needsDivider) { - if (!isDivider(getChildAt(childIndex))) { - addView(makeDividerView(), childIndex, makeDividerLayoutParams()); - } - hasDivider = true; - childIndex++; - } - - View childToAdd = itemData.getActionView(); - boolean needsPreDivider = false; - if (childToAdd != null) { - childToAdd.setLayoutParams(makeActionViewLayoutParams(childToAdd)); - } else { - ActionMenuItemView view = (ActionMenuItemView) itemData.getItemView( - MenuBuilder.TYPE_ACTION_BUTTON, this); - view.setItemInvoker(this); - needsPreDivider = i > 0 && !hasDivider && view.hasText() && - itemData.getIcon() == null; - needsDivider = view.hasText(); - childToAdd = view; - } - - boolean addPreDivider = removeChildrenUntil(childIndex, childToAdd, needsPreDivider); - - if (addPreDivider) addView(makeDividerView(), childIndex, makeDividerLayoutParams()); - if (needsPreDivider) childIndex++; - - if (getChildAt(childIndex) != childToAdd) { - addView(childToAdd, childIndex); - } - childIndex++; - } - - final boolean hasOverflow = mOverflowButton != null && mOverflowButton.getParent() == this; - final boolean needsOverflow = mReserveOverflow && mMenu.getNonActionItems(true).size() > 0; - - if (hasOverflow != needsOverflow) { - if (needsOverflow) { - if (mOverflowButton == null) { - OverflowMenuButton button = new OverflowMenuButton(mContext); - mOverflowButton = button; - } - boolean addDivider = removeChildrenUntil(childIndex, mOverflowButton, true); - if (addDivider && itemCount > 0) { - addView(makeDividerView(), childIndex, makeDividerLayoutParams()); - childIndex++; - } - addView(mOverflowButton, childIndex); - childIndex++; - } else { - removeView(mOverflowButton); - } - } else { - if (needsOverflow) { - boolean overflowDivider = itemCount > 0; - boolean addDivider = removeChildrenUntil(childIndex, mOverflowButton, - overflowDivider); - if (addDivider && itemCount > 0) { - addView(makeDividerView(), childIndex, makeDividerLayoutParams()); - } - if (overflowDivider) { - childIndex += 2; - } else { - childIndex++; - } - } - } - - while (getChildCount() > childIndex) { - removeViewAt(childIndex); - } - } - - private boolean removeChildrenUntil(int start, View targetChild, boolean needsPreDivider) { - final int childCount = getChildCount(); - boolean found = false; - for (int i = start; i < childCount; i++) { - final View child = getChildAt(i); - if (child == targetChild) { - found = true; - break; - } - } - - if (!found) { - return needsPreDivider; - } - - for (int i = start; i < getChildCount(); ) { - final View child = getChildAt(i); - if (needsPreDivider && isDivider(child)) { - needsPreDivider = false; - i++; - continue; - } - if (child == targetChild) break; - removeViewAt(i); - } - - return needsPreDivider; - } - - private static boolean isDivider(View v) { - return v != null && v.getId() == com.android.internal.R.id.action_menu_divider; - } - - public boolean showOverflowMenu() { - if (mOverflowButton != null && !isOverflowMenuShowing()) { - mMenu.getCallback().onMenuModeChange(mMenu); - return true; - } - return false; - } - - public void openOverflowMenu() { - OverflowPopup popup = new OverflowPopup(getContext(), mMenu, mOverflowButton, true); - mPostedOpenRunnable = new OpenOverflowRunnable(popup); - // Post this for later; we might still need a layout for the anchor to be right. - post(mPostedOpenRunnable); - } - - public boolean isOverflowMenuShowing() { - return mOverflowPopup != null && mOverflowPopup.isShowing(); - } - - public boolean isOverflowMenuOpen() { - return mOverflowPopup != null; - } - - public boolean hideOverflowMenu() { - if (mPostedOpenRunnable != null) { - removeCallbacks(mPostedOpenRunnable); - return true; + @Override + protected boolean hasDividerBeforeChildAt(int childIndex) { + final View childBefore = getChildAt(childIndex - 1); + final View child = getChildAt(childIndex); + boolean result = false; + if (childIndex < getChildCount() && childBefore instanceof ActionMenuChildView) { + result |= ((ActionMenuChildView) childBefore).needsDividerAfter(); } - - MenuPopupHelper popup = mOverflowPopup; - if (popup != null) { - popup.dismiss(); - return true; + if (childIndex > 0 && child instanceof ActionMenuChildView) { + result |= ((ActionMenuChildView) child).needsDividerBefore(); } - return false; - } - - private boolean addItemView(boolean needsDivider, ActionMenuItemView view) { - view.setItemInvoker(this); - boolean hasText = view.hasText(); - - if (hasText && needsDivider) { - addView(makeDividerView(), makeDividerLayoutParams()); - } - addView(view); - return hasText; - } - - private ImageView makeDividerView() { - ImageView result = new ImageView(mContext); - result.setImageDrawable(mDivider); - result.setScaleType(ImageView.ScaleType.FIT_XY); - result.setId(com.android.internal.R.id.action_menu_divider); return result; } - private LayoutParams makeDividerLayoutParams() { - LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.MATCH_PARENT); - params.topMargin = (int) mDividerPadding; - params.bottomMargin = (int) mDividerPadding; - return params; - } - - private LayoutParams makeActionViewLayoutParams(View view) { - return generateLayoutParams(view.getLayoutParams()); + public interface ActionMenuChildView { + public boolean needsDividerBefore(); + public boolean needsDividerAfter(); } - private class OverflowMenuButton extends ImageButton { - public OverflowMenuButton(Context context) { - super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle); + public static class LayoutParams extends LinearLayout.LayoutParams { + @ViewDebug.ExportedProperty(category = "layout") + public boolean isOverflowButton; - setClickable(true); - setFocusable(true); - setVisibility(VISIBLE); - setEnabled(true); + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); } - @Override - public boolean performClick() { - if (super.performClick()) { - return true; - } - - playSoundEffect(SoundEffectConstants.CLICK); - showOverflowMenu(); - return true; + public LayoutParams(LayoutParams other) { + super((LinearLayout.LayoutParams) other); + isOverflowButton = other.isOverflowButton; } - } - private class OverflowPopup extends MenuPopupHelper { - public OverflowPopup(Context context, MenuBuilder menu, View anchorView, - boolean overflowOnly) { - super(context, menu, anchorView, overflowOnly); + public LayoutParams(int width, int height) { + super(width, height); + isOverflowButton = false; } - @Override - public void onDismiss() { - super.onDismiss(); - mMenu.getCallback().onCloseMenu(mMenu, true); - mOverflowPopup = null; + public LayoutParams(int width, int height, boolean isOverflowButton) { + super(width, height); + this.isOverflowButton = isOverflowButton; } } } diff --git a/core/java/com/android/internal/view/menu/BaseMenuPresenter.java b/core/java/com/android/internal/view/menu/BaseMenuPresenter.java new file mode 100644 index 0000000..ddbb08c --- /dev/null +++ b/core/java/com/android/internal/view/menu/BaseMenuPresenter.java @@ -0,0 +1,203 @@ +/* + * 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; + } + + public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { + return false; + } + + public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { + 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..c337a5d 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) @@ -278,7 +282,7 @@ public final class IconMenuItemView extends TextView implements MenuView.ItemVie getLineBounds(0, tmpRect); mPositionIconAvailable.set(0, 0, getWidth(), tmpRect.top); Gravity.apply(Gravity.CENTER_VERTICAL | Gravity.LEFT, mIcon.getIntrinsicWidth(), mIcon - .getIntrinsicHeight(), mPositionIconAvailable, mPositionIconOutput); + .getIntrinsicHeight(), mPositionIconAvailable, mPositionIconOutput, isLayoutRtl()); mIcon.setBounds(mPositionIconOutput); } 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..f8d24a3 --- /dev/null +++ b/core/java/com/android/internal/view/menu/ListMenuPresenter.java @@ -0,0 +1,210 @@ +/* + * 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 boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { + return false; + } + + public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { + 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..fdfa954 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; - - /** The lazily loaded {@link MenuView} */ - private WeakReference<MenuView> mMenuView; + private boolean mIsClosing = false; - 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; - } + private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>(); - synchronized (this) { - MenuView menuView = mMenuView != null ? mMenuView.get() : null; - - if (menuView == null) { - menuView = (MenuView) getInflater().inflate( - LAYOUT_RES_FOR_TYPE[mMenuType], parent, false); - menuView.initialize(MenuBuilder.this, mMenuType); - - // Cache the view - mMenuView = new WeakReference<MenuView>(menuView); - - if (mFrozenViewStates != null) { - View view = (View) menuView; - view.restoreHierarchyState(mFrozenViewStates); - - // Clear this menu type's frozen state, since we just restored it - mFrozenViewStates.remove(view.getId()); - } - } - - return menuView; - } - } - - boolean hasMenuView() { - return mMenuView != null && mMenuView.get() != null; - } - } + private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters = + new CopyOnWriteArrayList<WeakReference<MenuPresenter>>(); /** - * Called by menu to notify of close and selection changes + * Called by menu to notify of close and selection changes. */ public interface Callback { /** @@ -315,30 +160,6 @@ public class MenuBuilder implements Menu { public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item); /** - * Called when a menu is closed. - * @param menu The menu that was closed. - * @param allMenusAreClosing Whether the menus are completely closing (true), - * or whether there is another menu opening shortly - * (false). For example, if the menu is closing because a - * sub menu is about to be shown, <var>allMenusAreClosing</var> - * is false. - */ - public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing); - - /** - * Called when a sub menu is selected. This is a cue to open the given sub menu's decor. - * @param subMenu the sub menu that is being opened - * @return whether the sub menu selection was handled by the callback - */ - public boolean onSubMenuSelected(SubMenuBuilder subMenu); - - /** - * Called when a sub menu is closed - * @param menu the sub menu that was closed - */ - public void onCloseSubMenu(SubMenuBuilder menu); - - /** * Called when the mode of the menu changes (for example, from icon to expanded). * * @param menu the menu that has changed modes @@ -354,8 +175,6 @@ public class MenuBuilder implements Menu { } public MenuBuilder(Context context) { - mMenuTypes = new MenuType[NUM_TYPES]; - mContext = context; mResources = context.getResources(); @@ -375,82 +194,68 @@ public class MenuBuilder implements Menu { mDefaultShowAsAction = defaultShowAsAction; return this; } - - public void setCallback(Callback callback) { - mCallback = callback; - } - MenuType getMenuType(int menuType) { - if (mMenuTypes[menuType] == null) { - mMenuTypes[menuType] = new MenuType(menuType); - } - - return mMenuTypes[menuType]; + /** + * Add a presenter to this menu. This will only hold a WeakReference; + * you do not need to explicitly remove a presenter, but you can using + * {@link #removeMenuPresenter(MenuPresenter)}. + * + * @param presenter The presenter to add + */ + public void addMenuPresenter(MenuPresenter presenter) { + mPresenters.add(new WeakReference<MenuPresenter>(presenter)); + presenter.initForMenu(mContext, this); + mIsActionItemsStale = true; } - + /** - * Gets a menu View that contains this menu's items. - * - * @param menuType The type of menu to get a View for (must be one of - * {@link #TYPE_ICON}, {@link #TYPE_EXPANDED}, - * {@link #TYPE_DIALOG}). - * @param parent The ViewGroup that provides a set of LayoutParams values - * for this menu view - * @return A View for the menu of type <var>menuType</var> + * Remove a presenter from this menu. That presenter will no longer + * receive notifications of updates to this menu's data. + * + * @param presenter The presenter to remove */ - public View getMenuView(int menuType, ViewGroup parent) { - // The expanded menu depends on the number if items shown in the icon menu (which - // is adjustable as setters/XML attributes on IconMenuView [imagine a larger LCD - // wanting to show more icons]). If, for example, the activity goes through - // an orientation change while the expanded menu is open, the icon menu's view - // won't have an instance anymore; so here we make sure we have an icon menu view (matching - // the same parent so the layout parameters from the XML are used). This - // will create the icon menu view and cache it (if it doesn't already exist). - if (menuType == TYPE_EXPANDED - && (mMenuTypes[TYPE_ICON] == null || !mMenuTypes[TYPE_ICON].hasMenuView())) { - getMenuType(TYPE_ICON).getMenuView(parent); + public void removeMenuPresenter(MenuPresenter presenter) { + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter item = ref.get(); + if (item == null || item == presenter) { + mPresenters.remove(ref); + } } - - return (View) getMenuType(menuType).getMenuView(parent); } - private int getNumIconMenuItemsShown() { - ViewGroup parent = null; - - if (!mMenuTypes[TYPE_ICON].hasMenuView()) { - /* - * There isn't an icon menu view instantiated, so when we get it - * below, it will lazily instantiate it. We should pass a proper - * parent so it uses the layout_ attributes present in the XML - * layout file. - */ - if (mMenuTypes[TYPE_EXPANDED].hasMenuView()) { - View expandedMenuView = (View) mMenuTypes[TYPE_EXPANDED].getMenuView(null); - parent = (ViewGroup) expandedMenuView.getParent(); + private void dispatchPresenterUpdate(boolean cleared) { + if (mPresenters.isEmpty()) return; + + stopDispatchingItemsChanged(); + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else { + presenter.updateMenuView(cleared); } } - - return ((IconMenuView) getMenuView(TYPE_ICON, parent)).getNumActualItemsShown(); + startDispatchingItemsChanged(); } - /** - * Clears the cached menu views. Call this if the menu views need to another - * layout (for example, if the screen size has changed). - */ - public void clearMenuViews() { - for (int i = NUM_TYPES - 1; i >= 0; i--) { - if (mMenuTypes[i] != null) { - mMenuTypes[i].mMenuView = null; - } - } - - for (int i = mItems.size() - 1; i >= 0; i--) { - MenuItemImpl item = mItems.get(i); - if (item.hasSubMenu()) { - ((SubMenuBuilder) item.getSubMenu()).clearMenuViews(); + private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu) { + if (mPresenters.isEmpty()) return false; + + boolean result = false; + + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else if (!result) { + result = presenter.onSubMenuSelected(subMenu); } - item.clearItemViews(); } + return result; + } + + public void setCallback(Callback cb) { + mCallback = cb; } /** @@ -468,7 +273,7 @@ public class MenuBuilder implements Menu { } mItems.add(findInsertIndex(mItems, ordering), item); - onItemsChanged(false); + onItemsChanged(true); return item; } @@ -554,7 +359,7 @@ public class MenuBuilder implements Menu { } // Notify menu views - onItemsChanged(false); + onItemsChanged(true); } } @@ -573,7 +378,7 @@ public class MenuBuilder implements Menu { mItems.remove(index); - if (updateChildrenOnMenuViews) onItemsChanged(false); + if (updateChildrenOnMenuViews) onItemsChanged(true); } public void removeItemAt(int index) { @@ -585,6 +390,7 @@ public class MenuBuilder implements Menu { clear(); clearHeader(); mPreventDispatchingItemsChanged = false; + mItemsChangedWhileDispatchPrevented = false; onItemsChanged(true); } @@ -725,19 +531,14 @@ public class MenuBuilder implements Menu { return mItems.get(index); } - public MenuItem getOverflowItem(int index) { - flagActionItems(true); - return mNonActionItems.get(index); - } - public boolean isShortcutKey(int keyCode, KeyEvent event) { return findItemWithShortcutForKey(keyCode, event) != null; } public void setQwertyMode(boolean isQwerty) { mQwertyMode = isQwerty; - - refreshShortcuts(isShortcutsVisible(), isQwerty); + + onItemsChanged(false); } /** @@ -751,8 +552,7 @@ public class MenuBuilder implements Menu { * @return An ordering integer that can be used to order this item across * all the items (even from other categories). */ - private static int getOrdering(int categoryOrder) - { + private static int getOrdering(int categoryOrder) { final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT; if (index < 0 || index >= sCategoryToOrder.length) { @@ -770,23 +570,6 @@ public class MenuBuilder implements Menu { } /** - * Refreshes the shortcut labels on each of the displayed items. Passes the arguments - * so submenus don't need to call their parent menu for the same values. - */ - private void refreshShortcuts(boolean shortcutsVisible, boolean qwertyMode) { - MenuItemImpl item; - for (int i = mItems.size() - 1; i >= 0; i--) { - item = mItems.get(i); - - if (item.hasSubMenu()) { - ((MenuBuilder) item.getSubMenu()).refreshShortcuts(shortcutsVisible, qwertyMode); - } - - item.refreshShortcutOnItemViews(shortcutsVisible, qwertyMode); - } - } - - /** * Sets whether the shortcuts should be visible on menus. Devices without hardware * key input will never make shortcuts visible even if this method is passed 'true'. * @@ -798,7 +581,7 @@ public class MenuBuilder implements Menu { if (mShortcutsVisible == shortcutsVisible) return; setShortcutsVisibleInner(shortcutsVisible); - refreshShortcuts(mShortcutsVisible, isQwertyMode()); + onItemsChanged(false); } private void setShortcutsVisibleInner(boolean shortcutsVisible) { @@ -818,15 +601,24 @@ public class MenuBuilder implements Menu { Resources getResources() { return mResources; } - - public Callback getCallback() { - return mCallback; - } public Context getContext() { return mContext; } + boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) { + return mCallback != null && mCallback.onMenuItemSelected(menu, item); + } + + /** + * Dispatch a mode change event to this menu's callback. + */ + public void changeMenuMode() { + if (mCallback != null) { + mCallback.onMenuModeChange(this); + } + } + private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) { for (int i = items.size() - 1; i >= 0; i--) { MenuItemImpl item = items.get(i); @@ -860,7 +652,7 @@ public class MenuBuilder implements Menu { * (the ALT-enabled char corresponds to the shortcut) associated * with the keyCode. */ - List<MenuItemImpl> findItemsWithShortcutForKey(int keyCode, KeyEvent event) { + void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) { final boolean qwerty = isQwertyMode(); final int metaState = event.getMetaState(); final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); @@ -868,18 +660,15 @@ public class MenuBuilder implements Menu { final boolean isKeyCodeMapped = event.getKeyData(possibleChars); // The delete key is not mapped to '\b' so we treat it specially if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) { - return null; + return; } - Vector<MenuItemImpl> items = new Vector(); // Look for an item whose shortcut is this key. final int N = mItems.size(); for (int i = 0; i < N; i++) { MenuItemImpl item = mItems.get(i); if (item.hasSubMenu()) { - List<MenuItemImpl> subMenuItems = ((MenuBuilder)item.getSubMenu()) - .findItemsWithShortcutForKey(keyCode, event); - items.addAll(subMenuItems); + ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event); } final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut(); if (((metaState & (KeyEvent.META_SHIFT_ON | KeyEvent.META_SYM_ON)) == 0) && @@ -892,7 +681,6 @@ public class MenuBuilder implements Menu { items.add(item); } } - return items; } /* @@ -908,9 +696,11 @@ public class MenuBuilder implements Menu { */ MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) { // Get all items that can be associated directly or indirectly with the keyCode - List<MenuItemImpl> items = findItemsWithShortcutForKey(keyCode, event); + ArrayList<MenuItemImpl> items = mTempShortcutItemList; + items.clear(); + findItemsWithShortcutForKey(items, keyCode, event); - if (items == null) { + if (items.isEmpty()) { return null; } @@ -920,15 +710,18 @@ public class MenuBuilder implements Menu { event.getKeyData(possibleChars); // If we have only one element, we can safely returns it - if (items.size() == 1) { + final int size = items.size(); + if (size == 1) { return items.get(0); } final boolean qwerty = isQwertyMode(); // If we found more than one item associated with the key, // we have to return the exact match - for (MenuItemImpl item : items) { - final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut(); + for (int i = 0; i < size; i++) { + final MenuItemImpl item = items.get(i); + final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : + item.getNumericShortcut(); if ((shortcutChar == possibleChars.meta[0] && (metaState & KeyEvent.META_ALT_ON) == 0) || (shortcutChar == possibleChars.meta[2] && @@ -951,18 +744,18 @@ public class MenuBuilder implements Menu { if (itemImpl == null || !itemImpl.isEnabled()) { return false; - } + } boolean invoked = itemImpl.invoke(); - if (item.hasSubMenu()) { + if (itemImpl.hasCollapsibleActionView()) { + invoked |= itemImpl.expandActionView(); + if (invoked) close(true); + } else 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 +775,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 +797,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 +840,7 @@ public class MenuBuilder implements Menu { */ void onItemVisibleChanged(MenuItemImpl item) { // Notify of items being changed + mIsVisibleItemsStale = true; onItemsChanged(false); } @@ -1034,6 +850,7 @@ public class MenuBuilder implements Menu { */ void onItemActionRequestChanged(MenuItemImpl item) { // Notify of items being changed + mIsActionItemsStale = true; onItemsChanged(false); } @@ -1055,17 +872,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 +896,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; + 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); } - 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); - } - } - } - - 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 +1077,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; } @@ -1402,108 +1085,41 @@ public class MenuBuilder implements Menu { 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; - } + public boolean expandItemActionView(MenuItemImpl item) { + if (mPresenters.isEmpty()) return false; - if (menuTypes[i].hasMenuView()) { - ((View) menuTypes[i].getMenuView(null)).saveHierarchyState(viewStates); - } - } - - outState.putSparseParcelableArray(VIEWS_TAG, viewStates); - } + boolean expanded = false; - 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; + stopDispatchingItemsChanged(); + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else if ((expanded = presenter.expandItemActionView(this, item))) { + break; } - - 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(); } + startDispatchingItemsChanged(); - public MenuItemImpl getItem(int position) { - return getVisibleItems().get(position + getOffset()); - } + return expanded; + } - 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 boolean collapseItemActionView(MenuItemImpl item) { + if (mPresenters.isEmpty()) return false; - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView != null) { - MenuView.ItemView itemView = (MenuView.ItemView) convertView; - itemView.getItemData().setItemView(mMenuType, null); + boolean collapsed = false; - 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); + stopDispatchingItemsChanged(); + for (WeakReference<MenuPresenter> ref : mPresenters) { + final MenuPresenter presenter = ref.get(); + if (presenter == null) { + mPresenters.remove(ref); + } else if ((collapsed = presenter.collapseItemActionView(this, item))) { + break; } } - } + startDispatchingItemsChanged(); - /** - * 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(); - } + return collapsed; } } diff --git a/core/java/com/android/internal/view/menu/MenuDialogHelper.java b/core/java/com/android/internal/view/menu/MenuDialogHelper.java index d7438d6..5c8e057 100644 --- a/core/java/com/android/internal/view/menu/MenuDialogHelper.java +++ b/core/java/com/android/internal/view/menu/MenuDialogHelper.java @@ -24,17 +24,20 @@ import android.view.KeyEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; -import android.widget.ListAdapter; /** * Helper for menus that appear as Dialogs (context and submenus). * * @hide */ -public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogInterface.OnClickListener { +public class MenuDialogHelper implements DialogInterface.OnKeyListener, + DialogInterface.OnClickListener, + DialogInterface.OnDismissListener, + MenuPresenter.Callback { private MenuBuilder mMenu; - private ListAdapter mAdapter; private AlertDialog mDialog; + ListMenuPresenter mPresenter; + private MenuPresenter.Callback mPresenterCallback; public MenuDialogHelper(MenuBuilder menu) { mMenu = menu; @@ -49,12 +52,15 @@ public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogIn // Many references to mMenu, create local reference final MenuBuilder menu = mMenu; - // Get an adapter for the menu item views - mAdapter = menu.getMenuAdapter(MenuBuilder.TYPE_DIALOG); - // Get the builder for the dialog - final AlertDialog.Builder builder = new AlertDialog.Builder(menu.getContext()) - .setAdapter(mAdapter, this); + final AlertDialog.Builder builder = new AlertDialog.Builder(menu.getContext()); + + mPresenter = new ListMenuPresenter(builder.getContext(), + com.android.internal.R.layout.list_menu_item_layout); + + mPresenter.setCallback(this); + mMenu.addMenuPresenter(mPresenter); + builder.setAdapter(mPresenter.getAdapter(), this); // Set the title final View headerView = menu.getHeaderView(); @@ -68,13 +74,10 @@ public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogIn // Set the key listener builder.setOnKeyListener(this); - - // Since this is for a menu, disable the recycling of views - // This is done by the menu framework anyway - builder.setRecycleOnMeasureEnabled(false); // Show the menu mDialog = builder.create(); + mDialog.setOnDismissListener(this); WindowManager.LayoutParams lp = mDialog.getWindow().getAttributes(); lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; @@ -122,6 +125,10 @@ public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogIn } + public void setPresenterCallback(MenuPresenter.Callback cb) { + mPresenterCallback = cb; + } + /** * Dismisses the menu's dialog. * @@ -132,9 +139,31 @@ public class MenuDialogHelper implements DialogInterface.OnKeyListener, DialogIn mDialog.dismiss(); } } - + + @Override + public void onDismiss(DialogInterface dialog) { + mPresenter.onCloseMenu(mMenu, true); + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + if (allMenusAreClosing || menu == mMenu) { + dismiss(); + } + if (mPresenterCallback != null) { + mPresenterCallback.onCloseMenu(menu, allMenusAreClosing); + } + } + + @Override + public boolean onOpenSubMenu(MenuBuilder subMenu) { + if (mPresenterCallback != null) { + return mPresenterCallback.onOpenSubMenu(subMenu); + } + return false; + } + public void onClick(DialogInterface dialog, int which) { - mMenu.performItemAction((MenuItemImpl) mAdapter.getItem(which), 0); + mMenu.performItemAction((MenuItemImpl) mPresenter.getAdapter().getItem(which), 0); } - } diff --git a/core/java/com/android/internal/view/menu/MenuItemImpl.java b/core/java/com/android/internal/view/menu/MenuItemImpl.java index 305115f..1a6cc54 100644 --- a/core/java/com/android/internal/view/menu/MenuItemImpl.java +++ b/core/java/com/android/internal/view/menu/MenuItemImpl.java @@ -16,21 +16,20 @@ 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.Context; 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; +import android.widget.LinearLayout; /** * @hide @@ -60,9 +59,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; @@ -83,6 +79,8 @@ public final class MenuItemImpl implements MenuItem { private int mShowAsAction = SHOW_AS_ACTION_NEVER; private View mActionView; + private OnActionExpandListener mOnActionExpandListener; + private boolean mIsActionViewExpanded = false; /** Used for the icon resource ID if this item does not have an icon */ static final int NO_ICON = 0; @@ -128,7 +126,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 +146,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 +167,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 +178,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 +227,7 @@ public final class MenuItemImpl implements MenuItem { mShortcutAlphabeticChar = Character.toLowerCase(alphaChar); - refreshShortcutOnItemViews(); + mMenu.onItemsChanged(false); return this; } @@ -256,7 +241,7 @@ public final class MenuItemImpl implements MenuItem { mShortcutNumericChar = numericChar; - refreshShortcutOnItemViews(); + mMenu.onItemsChanged(false); return this; } @@ -265,7 +250,7 @@ public final class MenuItemImpl implements MenuItem { mShortcutNumericChar = numericChar; mShortcutAlphabeticChar = Character.toLowerCase(alphaChar); - refreshShortcutOnItemViews(); + mMenu.onItemsChanged(false); return this; } @@ -322,38 +307,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 +347,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 +372,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 +392,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 +402,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 +415,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 +449,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 +485,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 +498,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 +527,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) { @@ -695,13 +556,70 @@ 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)); + final Context context = mMenu.getContext(); + final LayoutInflater inflater = LayoutInflater.from(context); + setActionView(inflater.inflate(resId, new LinearLayout(context))); return this; } public View getActionView() { return mActionView; } + + @Override + public MenuItem setShowAsActionFlags(int actionEnum) { + setShowAsAction(actionEnum); + return this; + } + + @Override + public boolean expandActionView() { + if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0 || mActionView == null) { + return false; + } + + if (mOnActionExpandListener == null || + mOnActionExpandListener.onMenuItemActionExpand(this)) { + return mMenu.expandItemActionView(this); + } + + return false; + } + + @Override + public boolean collapseActionView() { + if ((mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) == 0) { + return false; + } + if (mActionView == null) { + // We're already collapsed if we have no action view. + return true; + } + + if (mOnActionExpandListener == null || + mOnActionExpandListener.onMenuItemActionCollapse(this)) { + return mMenu.collapseItemActionView(this); + } + + return false; + } + + @Override + public MenuItem setOnActionExpandListener(OnActionExpandListener listener) { + mOnActionExpandListener = listener; + return this; + } + + public boolean hasCollapsibleActionView() { + return (mShowAsAction & SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) != 0 && mActionView != null; + } + + public void setActionViewExpanded(boolean isExpanded) { + mIsActionViewExpanded = isExpanded; + mMenu.onItemsChanged(false); + } + + public boolean isActionViewExpanded() { + return mIsActionViewExpanded; + } } diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java index 04a059e..5767519 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,99 @@ 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; + } + + public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { + return false; + } + + public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { + 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..bd66448 --- /dev/null +++ b/core/java/com/android/internal/view/menu/MenuPresenter.java @@ -0,0 +1,128 @@ +/* + * 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(); + + /** + * Called when a menu item with a collapsable action view should expand its action view. + * + * @param menu Menu containing the item to be expanded + * @param item Item to be expanded + * @return true if this presenter expanded the action view, false otherwise. + */ + public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item); + + /** + * Called when a menu item with a collapsable action view should collapse its action view. + * + * @param menu Menu containing the item to be collapsed + * @param item Item to be collapsed + * @return true if this presenter collapsed the action view, false otherwise. + */ + public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item); +} 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..fb1cd5e 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); } @@ -81,6 +76,12 @@ public class SubMenuBuilder extends MenuBuilder implements SubMenu { return mParentMenu; } + @Override + boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) { + return super.dispatchMenuItemSelected(menu, item) || + mParentMenu.dispatchMenuItemSelected(menu, item); + } + public SubMenu setIcon(Drawable icon) { mItem.setIcon(icon); return this; @@ -110,5 +111,14 @@ public class SubMenuBuilder extends MenuBuilder implements SubMenu { public SubMenu setHeaderView(View view) { return (SubMenu) super.setHeaderViewInt(view); } - + + @Override + public boolean expandItemActionView(MenuItemImpl item) { + return mParentMenu.expandItemActionView(item); + } + + @Override + public boolean collapseItemActionView(MenuItemImpl item) { + return mParentMenu.collapseItemActionView(item); + } } diff --git a/core/java/com/android/internal/widget/AbsActionBarView.java b/core/java/com/android/internal/widget/AbsActionBarView.java new file mode 100644 index 0000000..ccbce3e --- /dev/null +++ b/core/java/com/android/internal/widget/AbsActionBarView.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget; + +import com.android.internal.view.menu.ActionMenuPresenter; +import com.android.internal.view.menu.ActionMenuView; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; + +public abstract class AbsActionBarView extends ViewGroup { + protected ActionMenuView mMenuView; + protected ActionMenuPresenter mActionMenuPresenter; + protected ActionBarContainer mSplitView; + + protected Animator mVisibilityAnim; + protected final VisibilityAnimListener mVisAnimListener = new VisibilityAnimListener(); + + private static final TimeInterpolator sAlphaInterpolator = new DecelerateInterpolator(); + + private static final int FADE_DURATION = 200; + + public AbsActionBarView(Context context) { + super(context); + } + + public AbsActionBarView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AbsActionBarView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setSplitView(ActionBarContainer splitView) { + mSplitView = splitView; + } + + public void animateToVisibility(int visibility) { + if (mVisibilityAnim != null) { + mVisibilityAnim.cancel(); + } + if (visibility == VISIBLE) { + if (getVisibility() != VISIBLE) { + setAlpha(0); + if (mSplitView != null && mMenuView != null) { + mMenuView.setAlpha(0); + } + } + ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 1); + anim.setDuration(FADE_DURATION); + anim.setInterpolator(sAlphaInterpolator); + if (mSplitView != null && mMenuView != null) { + AnimatorSet set = new AnimatorSet(); + ObjectAnimator splitAnim = ObjectAnimator.ofFloat(mMenuView, "alpha", 1); + splitAnim.setDuration(FADE_DURATION); + set.addListener(mVisAnimListener.withFinalVisibility(visibility)); + set.play(anim).with(splitAnim); + set.start(); + } else { + anim.addListener(mVisAnimListener.withFinalVisibility(visibility)); + anim.start(); + } + } else { + ObjectAnimator anim = ObjectAnimator.ofFloat(this, "alpha", 0); + anim.setDuration(FADE_DURATION); + anim.setInterpolator(sAlphaInterpolator); + if (mSplitView != null && mMenuView != null) { + AnimatorSet set = new AnimatorSet(); + ObjectAnimator splitAnim = ObjectAnimator.ofFloat(mMenuView, "alpha", 0); + splitAnim.setDuration(FADE_DURATION); + set.addListener(mVisAnimListener.withFinalVisibility(visibility)); + set.play(anim).with(splitAnim); + set.start(); + } else { + anim.addListener(mVisAnimListener.withFinalVisibility(visibility)); + anim.start(); + } + } + } + + @Override + public void setVisibility(int visibility) { + if (mVisibilityAnim != null) { + mVisibilityAnim.end(); + } + super.setVisibility(visibility); + } + + public boolean showOverflowMenu() { + if (mActionMenuPresenter != null) { + return mActionMenuPresenter.showOverflowMenu(); + } + return false; + } + + public void postShowOverflowMenu() { + post(new Runnable() { + public void run() { + showOverflowMenu(); + } + }); + } + + public boolean hideOverflowMenu() { + if (mActionMenuPresenter != null) { + return mActionMenuPresenter.hideOverflowMenu(); + } + return false; + } + + public boolean isOverflowMenuShowing() { + if (mActionMenuPresenter != null) { + return mActionMenuPresenter.isOverflowMenuShowing(); + } + return false; + } + + public boolean isOverflowReserved() { + return mActionMenuPresenter != null && mActionMenuPresenter.isOverflowReserved(); + } + + public void dismissPopupMenus() { + if (mActionMenuPresenter != null) { + mActionMenuPresenter.dismissPopupMenus(); + } + } + + protected int measureChildView(View child, int availableWidth, int childSpecHeight, + int spacing) { + child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), + childSpecHeight); + + availableWidth -= child.getMeasuredWidth(); + availableWidth -= spacing; + + return Math.max(0, availableWidth); + } + + protected int positionChild(View child, int x, int y, int contentHeight) { + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + int childTop = y + (contentHeight - childHeight) / 2; + + child.layout(x, childTop, x + childWidth, childTop + childHeight); + + return childWidth; + } + + protected int positionChildInverse(View child, int x, int y, int contentHeight) { + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + int childTop = y + (contentHeight - childHeight) / 2; + + child.layout(x - childWidth, childTop, x, childTop + childHeight); + + return childWidth; + } + + protected class VisibilityAnimListener implements Animator.AnimatorListener { + private boolean mCanceled = false; + private int mFinalVisibility; + + public VisibilityAnimListener withFinalVisibility(int visibility) { + mFinalVisibility = visibility; + return this; + } + + @Override + public void onAnimationStart(Animator animation) { + setVisibility(VISIBLE); + mVisibilityAnim = animation; + mCanceled = false; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCanceled) return; + + mVisibilityAnim = null; + setVisibility(mFinalVisibility); + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + } +} diff --git a/core/java/com/android/internal/widget/ActionBarContainer.java b/core/java/com/android/internal/widget/ActionBarContainer.java index c9b0ec9..d710cfa 100644 --- a/core/java/com/android/internal/widget/ActionBarContainer.java +++ b/core/java/com/android/internal/widget/ActionBarContainer.java @@ -16,10 +16,13 @@ package com.android.internal.widget; +import android.app.ActionBar; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import android.view.ActionMode; import android.view.MotionEvent; +import android.view.View; import android.widget.FrameLayout; /** @@ -29,6 +32,8 @@ import android.widget.FrameLayout; */ public class ActionBarContainer extends FrameLayout { private boolean mIsTransitioning; + private View mTabContainer; + private ActionBarView mActionBarView; public ActionBarContainer(Context context) { this(context, null); @@ -43,6 +48,12 @@ public class ActionBarContainer extends FrameLayout { a.recycle(); } + @Override + public void onFinishInflate() { + super.onFinishInflate(); + mActionBarView = (ActionBarView) findViewById(com.android.internal.R.id.action_bar); + } + /** * Set the action bar into a "transitioning" state. While transitioning * the bar will block focus and touch from all of its descendants. This @@ -65,6 +76,78 @@ 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; + if (tabView != null) { + addView(tabView); + } + } + + public View getTabContainer() { + return mTabContainer; + } + + @Override + public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) { + // No starting an action mode for an action bar child! (Where would it go?) + return null; + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int nonTabHeight = 0; + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + + if (child == mTabContainer) continue; + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + nonTabHeight = Math.max(nonTabHeight, + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); + } + + if (mTabContainer != null && mTabContainer.getVisibility() != GONE) { + final int mode = MeasureSpec.getMode(heightMeasureSpec); + if (mode == MeasureSpec.AT_MOST) { + final int maxHeight = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), + Math.min(nonTabHeight + 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(); + final int tabHeight = mTabContainer.getMeasuredHeight(); + + if ((mActionBarView.getDisplayOptions() & ActionBar.DISPLAY_SHOW_HOME) == 0) { + // Not showing home, put tabs on top. + final int count = getChildCount(); + for (int i = 0; i < count; i++){ + final View child = getChildAt(i); + + if (child == mTabContainer) continue; + + child.offsetTopAndBottom(tabHeight); + } + mTabContainer.layout(l, 0, r, tabHeight); + } else { + mTabContainer.layout(l, containerHeight - tabHeight, r, containerHeight); + } + } + } } diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java index f762265..fc43994 100644 --- a/core/java/com/android/internal/widget/ActionBarContextView.java +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -15,29 +15,30 @@ */ package com.android.internal.widget; -import com.android.internal.R; -import com.android.internal.view.menu.ActionMenuView; -import com.android.internal.view.menu.MenuBuilder; - import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.content.res.TypedArray; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.LinearLayout; import android.widget.TextView; +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; + /** * @hide */ -public class ActionBarContextView extends ViewGroup implements AnimatorListener { +public class ActionBarContextView extends AbsActionBarView implements AnimatorListener { private static final String TAG = "ActionBarContextView"; private int mContentHeight; @@ -52,7 +53,6 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener private TextView mSubtitleView; private int mTitleStyleRes; private int mSubtitleStyleRes; - private ActionMenuView mMenuView; private Animator mCurrentAnimation; private boolean mAnimateInOnLayout; @@ -85,12 +85,6 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener com.android.internal.R.styleable.ActionMode_height, 0); a.recycle(); } - - @Override - public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) { - // No starting an action mode for an existing action mode UI child! (Where would it go?) - return null; - } public void setHeight(int height) { mContentHeight = height; @@ -136,27 +130,24 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener mTitleLayout = (LinearLayout) getChildAt(getChildCount() - 1); mTitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_title); mSubtitleView = (TextView) mTitleLayout.findViewById(R.id.action_bar_subtitle); - if (mTitle != null) { - mTitleView.setText(mTitle); - if (mTitleStyleRes != 0) { - mTitleView.setTextAppearance(mContext, mTitleStyleRes); - } + if (mTitleStyleRes != 0) { + mTitleView.setTextAppearance(mContext, mTitleStyleRes); } - if (mSubtitle != null) { - mSubtitleView.setText(mSubtitle); - if (mSubtitleStyleRes != 0) { - mSubtitleView.setTextAppearance(mContext, mSubtitleStyleRes); - } - mSubtitleView.setVisibility(VISIBLE); - } - } else { - mTitleView.setText(mTitle); - mSubtitleView.setText(mSubtitle); - mSubtitleView.setVisibility(mSubtitle != null ? VISIBLE : GONE); - if (mTitleLayout.getParent() == null) { - addView(mTitleLayout); + if (mSubtitleStyleRes != 0) { + mSubtitleView.setTextAppearance(mContext, mSubtitleStyleRes); } } + + mTitleView.setText(mTitle); + mSubtitleView.setText(mSubtitle); + + final boolean hasTitle = !TextUtils.isEmpty(mTitle); + final boolean hasSubtitle = !TextUtils.isEmpty(mSubtitle); + mSubtitleView.setVisibility(hasSubtitle ? VISIBLE : GONE); + mTitleLayout.setVisibility(hasTitle || hasSubtitle ? VISIBLE : GONE); + if (mTitleLayout.getParent() == null) { + addView(mTitleLayout); + } } public void initForMode(final ActionMode mode) { @@ -176,10 +167,28 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener }); final MenuBuilder menu = (MenuBuilder) mode.getMenu(); - mMenuView = (ActionMenuView) menu.getMenuView(MenuBuilder.TYPE_ACTION_BUTTON, this); - mMenuView.setOverflowReserved(true); - mMenuView.updateChildren(false); - addView(mMenuView); + mActionMenuPresenter = new ActionMenuPresenter(); + mActionMenuPresenter.setReserveOverflow(true); + + final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT); + if (mSplitView == null) { + menu.addMenuPresenter(mActionMenuPresenter); + mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this); + addView(mMenuView, layoutParams); + } else { + // Allow full screen width in split mode. + mActionMenuPresenter.setWidthLimit( + getContext().getResources().getDisplayMetrics().widthPixels, true); + // No limit to the item count; use whatever will fit. + mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE); + // Span the whole width + layoutParams.width = LayoutParams.MATCH_PARENT; + layoutParams.height = mContentHeight; + menu.addMenuPresenter(mActionMenuPresenter); + mMenuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this); + mSplitView.addView(mMenuView, layoutParams); + } mAnimateInOnLayout = true; } @@ -211,34 +220,34 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener public void killMode() { finishAnimation(); removeAllViews(); + if (mSplitView != null) { + mSplitView.removeView(mMenuView); + } mCustomView = null; mMenuView = null; mAnimateInOnLayout = false; } + @Override 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(); - } - } - + @Override public boolean hideOverflowMenu() { - if (mMenuView != null) { - return mMenuView.hideOverflowMenu(); + if (mActionMenuPresenter != null) { + return mActionMenuPresenter.hideOverflowMenu(); } return false; } + @Override public boolean isOverflowMenuShowing() { - if (mMenuView != null) { - return mMenuView.isOverflowMenuShowing(); + if (mActionMenuPresenter != null) { + return mActionMenuPresenter.isOverflowMenuShowing(); } return false; } @@ -346,7 +355,7 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener private Animator makeOutAnimation() { ObjectAnimator buttonAnimator = ObjectAnimator.ofFloat(mClose, "translationX", - 0, -mClose.getWidth()); + -mClose.getWidth()); buttonAnimator.setDuration(200); buttonAnimator.addListener(this); buttonAnimator.setInterpolator(new DecelerateInterpolator()); @@ -360,7 +369,7 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener for (int i = 0; i < 0; i++) { View child = mMenuView.getChildAt(i); child.setScaleY(0); - ObjectAnimator a = ObjectAnimator.ofFloat(child, "scaleY", 1, 0); + ObjectAnimator a = ObjectAnimator.ofFloat(child, "scaleY", 0); a.setDuration(100); a.setStartDelay(i * 70); b.with(a); @@ -387,7 +396,7 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener mAnimateInOnLayout = false; } } - + if (mTitleLayout != null && mCustomView == null) { x += positionChild(mTitleLayout, x, y, contentHeight); } @@ -403,36 +412,6 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener } } - private int measureChildView(View child, int availableWidth, int childSpecHeight, int spacing) { - child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), - childSpecHeight); - - availableWidth -= child.getMeasuredWidth(); - availableWidth -= spacing; - - return Math.max(0, availableWidth); - } - - private int positionChild(View child, int x, int y, int contentHeight) { - int childWidth = child.getMeasuredWidth(); - int childHeight = child.getMeasuredHeight(); - int childTop = y + (contentHeight - childHeight) / 2; - - child.layout(x, childTop, x + childWidth, childTop + childHeight); - - return childWidth; - } - - private int positionChildInverse(View child, int x, int y, int contentHeight) { - int childWidth = child.getMeasuredWidth(); - int childHeight = child.getMeasuredHeight(); - int childTop = y + (contentHeight - childHeight) / 2; - - child.layout(x - childWidth, childTop, x, childTop + childHeight); - - return childWidth; - } - @Override public void onAnimationStart(Animator animation) { } @@ -452,4 +431,9 @@ public class ActionBarContextView extends ViewGroup implements AnimatorListener @Override public void onAnimationRepeat(Animator animation) { } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } } diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index 891557d..e3286dd 100644 --- a/core/java/com/android/internal/widget/ActionBarView.java +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -18,8 +18,13 @@ 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.MenuItemImpl; +import com.android.internal.view.menu.MenuPresenter; +import com.android.internal.view.menu.MenuView; +import com.android.internal.view.menu.SubMenuBuilder; import android.app.ActionBar; import android.app.ActionBar.OnNavigationListener; @@ -28,25 +33,25 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; import android.text.TextUtils; -import android.text.TextUtils.TruncateAt; import android.util.AttributeSet; +import android.util.DisplayMetrics; import android.util.Log; -import android.view.ActionMode; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.Window; import android.widget.AdapterView; import android.widget.FrameLayout; -import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; @@ -57,7 +62,7 @@ import android.widget.TextView; /** * @hide */ -public class ActionBarView extends ViewGroup { +public class ActionBarView extends AbsActionBarView { private static final String TAG = "ActionBarView"; /** @@ -77,7 +82,7 @@ public class ActionBarView extends ViewGroup { private static final int DEFAULT_CUSTOM_GRAVITY = Gravity.LEFT | Gravity.CENTER_VERTICAL; - private final int mContentHeight; + private int mContentHeight; private int mNavigationMode; private int mDisplayOptions = ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_HOME_AS_UP; @@ -85,18 +90,15 @@ public class ActionBarView extends ViewGroup { private CharSequence mSubtitle; private Drawable mIcon; private Drawable mLogo; - private Drawable mDivider; - private View mHomeLayout; - private View mHomeAsUpView; - private ImageView mIconView; + private HomeView mHomeLayout; + private HomeView mExpandedHomeLayout; private LinearLayout mTitleLayout; private TextView mTitleView; private TextView mSubtitleView; private Spinner mSpinner; private LinearLayout mListNavLayout; - private HorizontalScrollView mTabScrollView; - private LinearLayout mTabLayout; + private ScrollingTabContainerView mTabScrollView; private View mCustomNavView; private ProgressBar mProgressView; private ProgressBar mIndeterminateProgressView; @@ -109,11 +111,12 @@ public class ActionBarView extends ViewGroup { private int mProgressStyle; private int mIndeterminateProgressStyle; - private boolean mShowMenu; + private boolean mSplitActionBar; private boolean mUserTitle; + private boolean mIncludeTabs; + private boolean mIsCollapsable; private MenuBuilder mOptionsMenu; - private ActionMenuView mMenuView; private ActionBarContextView mContextView; @@ -122,6 +125,11 @@ public class ActionBarView extends ViewGroup { private SpinnerAdapter mSpinnerAdapter; private OnNavigationListener mCallback; + private Runnable mTabSelector; + + private ExpandedActionViewMenuPresenter mExpandedMenuPresenter; + View mExpandedActionView; + private final AdapterView.OnItemSelectedListener mNavItemSelectedListener = new AdapterView.OnItemSelectedListener() { public void onItemSelected(AdapterView parent, View view, int position, long id) { @@ -134,7 +142,15 @@ public class ActionBarView extends ViewGroup { } }; - private OnClickListener mTabClickListener = null; + private final OnClickListener mExpandedActionViewUpListener = new OnClickListener() { + @Override + public void onClick(View v) { + final MenuItemImpl item = mExpandedMenuPresenter.mCurrentExpandedItem; + if (item != null) { + item.collapseActionView(); + } + } + }; public ActionBarView(Context context, AttributeSet attrs) { super(context, attrs); @@ -185,10 +201,11 @@ public class ActionBarView extends ViewGroup { com.android.internal.R.styleable.ActionBar_homeLayout, com.android.internal.R.layout.action_bar_home); - mHomeLayout = inflater.inflate(homeResId, this, false); + mHomeLayout = (HomeView) inflater.inflate(homeResId, this, false); - mHomeAsUpView = mHomeLayout.findViewById(com.android.internal.R.id.up); - mIconView = (ImageView) mHomeLayout.findViewById(com.android.internal.R.id.home); + mExpandedHomeLayout = (HomeView) inflater.inflate(homeResId, this, false); + mExpandedHomeLayout.setUp(true); + mExpandedHomeLayout.setOnClickListener(mExpandedActionViewUpListener); mTitleStyleRes = a.getResourceId(R.styleable.ActionBar_titleTextStyle, 0); mSubtitleStyleRes = a.getResourceId(R.styleable.ActionBar_subtitleTextStyle, 0); @@ -210,8 +227,6 @@ public class ActionBarView extends ViewGroup { mContentHeight = a.getLayoutDimension(R.styleable.ActionBar_height, 0); - mDivider = a.getDrawable(R.styleable.ActionBar_divider); - a.recycle(); mLogoNavItem = new ActionMenuItem(context, 0, android.R.id.home, 0, 0, mTitle); @@ -228,6 +243,17 @@ public class ActionBarView extends ViewGroup { mHomeLayout.setFocusable(true); } + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + removeCallbacks(mTabSelector); + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + public void initProgress() { mProgressView = new ProgressBar(mContext, null, 0, mProgressStyle); mProgressView.setId(R.id.progress_horizontal); @@ -242,77 +268,97 @@ public class ActionBarView extends ViewGroup { addView(mIndeterminateProgressView); } - @Override - public ActionMode startActionModeForChild(View child, ActionMode.Callback callback) { - // No starting an action mode for an action bar child! (Where would it go?) - return null; + public void setContentHeight(int height) { + mContentHeight = height; + requestLayout(); } - public void setCallback(OnNavigationListener callback) { - mCallback = callback; + public int getContentHeight() { + return mContentHeight; } - public void setMenu(Menu menu) { - if (menu == mOptionsMenu) return; - - MenuBuilder builder = (MenuBuilder) menu; - mOptionsMenu = builder; - if (mMenuView != null) { - removeView(mMenuView); + public void setSplitActionBar(boolean splitActionBar) { + if (mSplitActionBar != splitActionBar) { + if (mMenuView != null) { + if (splitActionBar) { + removeView(mMenuView); + if (mSplitView != null) { + mSplitView.addView(mMenuView); + } + } else { + addView(mMenuView); + } + } + mSplitActionBar = splitActionBar; } - final ActionMenuView menuView = (ActionMenuView) builder.getMenuView( - MenuBuilder.TYPE_ACTION_BUTTON, null); - final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.MATCH_PARENT); - menuView.setLayoutParams(layoutParams); - addView(menuView); - mMenuView = menuView; } - public boolean showOverflowMenu() { - if (mMenuView != null) { - return mMenuView.showOverflowMenu(); - } - return false; + public boolean isSplitActionBar() { + return mSplitActionBar; } - public void openOverflowMenu() { - if (mMenuView != null) { - mMenuView.openOverflowMenu(); - } + public boolean hasEmbeddedTabs() { + return mIncludeTabs; } - public void postShowOverflowMenu() { - post(new Runnable() { - public void run() { - showOverflowMenu(); - } - }); + public void setEmbeddedTabView(ScrollingTabContainerView tabs) { + mTabScrollView = tabs; + mIncludeTabs = tabs != null; + if (mIncludeTabs && mNavigationMode == ActionBar.NAVIGATION_MODE_TABS) { + addView(mTabScrollView); + } } - public boolean hideOverflowMenu() { - if (mMenuView != null) { - return mMenuView.hideOverflowMenu(); - } - return false; + public void setCallback(OnNavigationListener callback) { + mCallback = callback; } - public boolean isOverflowMenuShowing() { - if (mMenuView != null) { - return mMenuView.isOverflowMenuShowing(); + public void setMenu(Menu menu, MenuPresenter.Callback cb) { + if (menu == mOptionsMenu) return; + + if (mOptionsMenu != null) { + mOptionsMenu.removeMenuPresenter(mActionMenuPresenter); + mOptionsMenu.removeMenuPresenter(mExpandedMenuPresenter); } - return false; - } - public boolean isOverflowMenuOpen() { + MenuBuilder builder = (MenuBuilder) menu; + mOptionsMenu = builder; if (mMenuView != null) { - return mMenuView.isOverflowMenuOpen(); + removeView(mMenuView); + } + if (mActionMenuPresenter == null) { + mActionMenuPresenter = new ActionMenuPresenter(); + mActionMenuPresenter.setCallback(cb); + mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter(); } - return false; - } - public boolean isOverflowReserved() { - return mMenuView != null && mMenuView.isOverflowReserved(); + ActionMenuView menuView; + final LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.MATCH_PARENT); + if (!mSplitActionBar) { + builder.addMenuPresenter(mActionMenuPresenter); + builder.addMenuPresenter(mExpandedMenuPresenter); + menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this); + addView(menuView, layoutParams); + } else { + // Allow full screen width in split mode. + mActionMenuPresenter.setWidthLimit( + getContext().getResources().getDisplayMetrics().widthPixels, true); + // No limit to the item count; use whatever will fit. + mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE); + // Span the whole width + layoutParams.width = LayoutParams.MATCH_PARENT; + builder.addMenuPresenter(mActionMenuPresenter); + builder.addMenuPresenter(mExpandedMenuPresenter); + menuView = (ActionMenuView) mActionMenuPresenter.getMenuView(this); + if (mSplitView != null) { + mSplitView.addView(menuView, layoutParams); + } else { + // We'll add this later if we missed it this time. + menuView.setLayoutParams(layoutParams); + } + } + mMenuView = menuView; } public void setCustomNavigationView(View view) { @@ -382,18 +428,23 @@ public class ActionBarView extends ViewGroup { public void setDisplayOptions(int options) { final int flagsChanged = options ^ mDisplayOptions; mDisplayOptions = options; + + if ((flagsChanged & ActionBar.DISPLAY_DISABLE_HOME) != 0) { + final boolean disableHome = (options & ActionBar.DISPLAY_DISABLE_HOME) != 0; + mHomeLayout.setEnabled(!disableHome); + } + if ((flagsChanged & DISPLAY_RELAYOUT_MASK) != 0) { final int vis = (options & ActionBar.DISPLAY_SHOW_HOME) != 0 ? VISIBLE : GONE; mHomeLayout.setVisibility(vis); if ((flagsChanged & ActionBar.DISPLAY_HOME_AS_UP) != 0) { - mHomeAsUpView.setVisibility((options & ActionBar.DISPLAY_HOME_AS_UP) != 0 - ? VISIBLE : GONE); + mHomeLayout.setUp((options & ActionBar.DISPLAY_HOME_AS_UP) != 0); } if ((flagsChanged & ActionBar.DISPLAY_USE_LOGO) != 0) { final boolean logoVis = mLogo != null && (options & ActionBar.DISPLAY_USE_LOGO) != 0; - mIconView.setImageDrawable(logoVis ? mLogo : mIcon); + mHomeLayout.setIcon(logoVis ? mLogo : mIcon); } if ((flagsChanged & ActionBar.DISPLAY_SHOW_TITLE) != 0) { @@ -416,6 +467,59 @@ public class ActionBarView extends ViewGroup { } else { invalidate(); } + + // Make sure the home button has an accurate content description for accessibility. + if ((options & ActionBar.DISPLAY_DISABLE_HOME) != 0) { + mHomeLayout.setContentDescription(null); + } else if ((options & ActionBar.DISPLAY_HOME_AS_UP) != 0) { + mHomeLayout.setContentDescription(mContext.getResources().getText( + R.string.action_bar_up_description)); + } else { + mHomeLayout.setContentDescription(mContext.getResources().getText( + R.string.action_bar_home_description)); + } + } + + public void setIcon(Drawable icon) { + mIcon = icon; + if (icon != null && + ((mDisplayOptions & ActionBar.DISPLAY_USE_LOGO) == 0 || mLogo == null)) { + mHomeLayout.setIcon(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) { + mHomeLayout.setIcon(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 - + mHomeLayout.getVerticalIconPadding(); + 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) { @@ -423,12 +527,12 @@ public class ActionBarView extends ViewGroup { if (mode != oldMode) { switch (oldMode) { case ActionBar.NAVIGATION_MODE_LIST: - if (mSpinner != null) { + if (mListNavLayout != null) { removeView(mListNavLayout); } break; case ActionBar.NAVIGATION_MODE_TABS: - if (mTabLayout != null) { + if (mTabScrollView != null && mIncludeTabs) { removeView(mTabScrollView); } } @@ -452,23 +556,26 @@ public class ActionBarView extends ViewGroup { addView(mListNavLayout); break; case ActionBar.NAVIGATION_MODE_TABS: - ensureTabsExist(); - addView(mTabScrollView); + if (mTabScrollView != null && mIncludeTabs) { + addView(mTabScrollView); + } break; } mNavigationMode = mode; requestLayout(); } } - - private void ensureTabsExist() { - if (mTabScrollView == null) { - mTabScrollView = new HorizontalScrollView(getContext()); - mTabScrollView.setHorizontalFadingEdgeEnabled(true); - mTabLayout = new LinearLayout(getContext(), null, - com.android.internal.R.attr.actionBarTabBarStyle); - mTabScrollView.addView(mTabLayout); - } + + public ScrollingTabContainerView createTabContainer() { + final LinearLayout tabLayout = new LinearLayout(getContext(), null, + com.android.internal.R.attr.actionBarTabBarStyle); + tabLayout.setMeasureWithLargestChildEnabled(true); + tabLayout.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, mContentHeight)); + + final ScrollingTabContainerView scroller = new ScrollingTabContainerView(mContext); + scroller.setTabLayout(tabLayout); + return scroller; } public void setDropdownAdapter(SpinnerAdapter adapter) { @@ -502,47 +609,6 @@ public class ActionBarView extends ViewGroup { return mDisplayOptions; } - private TabView createTabView(ActionBar.Tab tab) { - final TabView tabView = new TabView(getContext(), tab); - tabView.setFocusable(true); - - if (mTabClickListener == null) { - mTabClickListener = new TabClickListener(); - } - tabView.setOnClickListener(mTabClickListener); - return tabView; - } - - public void addTab(ActionBar.Tab tab, boolean setSelected) { - ensureTabsExist(); - View tabView = createTabView(tab); - mTabLayout.addView(tabView); - if (setSelected) { - tabView.setSelected(true); - } - } - - public void addTab(ActionBar.Tab tab, int position, boolean setSelected) { - ensureTabsExist(); - final TabView tabView = createTabView(tab); - mTabLayout.addView(tabView, position); - if (setSelected) { - tabView.setSelected(true); - } - } - - public void removeTabAt(int position) { - if (mTabLayout != null) { - mTabLayout.removeViewAt(position); - } - } - - public void removeAllTabs() { - if (mTabLayout != null) { - mTabLayout.removeAllViews(); - } - } - @Override protected LayoutParams generateDefaultLayoutParams() { // Used by custom nav views if they don't supply layout params. Everything else @@ -591,21 +657,34 @@ public class ActionBarView extends ViewGroup { addView(mTitleLayout); } - public void setTabSelected(int position) { - ensureTabsExist(); - final int tabCount = mTabLayout.getChildCount(); - for (int i = 0; i < tabCount; i++) { - final View child = mTabLayout.getChildAt(i); - child.setSelected(i == position); - } - } - public void setContextView(ActionBarContextView view) { mContextView = view; } + public void setCollapsable(boolean collapsable) { + mIsCollapsable = collapsable; + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int childCount = getChildCount(); + if (mIsCollapsable) { + int visibleChildren = 0; + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE && + !(child == mMenuView && mMenuView.getChildCount() == 0)) { + visibleChildren++; + } + } + + if (visibleChildren == 0) { + // No size for an empty action bar when collapsable. + setMeasuredDimension(0, 0); + return; + } + } + int widthMode = MeasureSpec.getMode(widthMeasureSpec); if (widthMode != MeasureSpec.EXACTLY) { throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + @@ -633,54 +712,59 @@ public class ActionBarView extends ViewGroup { int leftOfCenter = availableWidth / 2; int rightOfCenter = leftOfCenter; - if (mHomeLayout.getVisibility() != GONE) { - mHomeLayout.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), + View homeLayout = mExpandedActionView != null ? mExpandedHomeLayout : mHomeLayout; + + if (homeLayout.getVisibility() != GONE) { + homeLayout.measure( + MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); - final int homeWidth = mHomeLayout.getMeasuredWidth(); + final int homeWidth = homeLayout.getMeasuredWidth(); availableWidth = Math.max(0, availableWidth - homeWidth); leftOfCenter = Math.max(0, availableWidth - homeWidth); } - if (mMenuView != null) { + if (mMenuView != null && mMenuView.getParent() == this) { availableWidth = measureChildView(mMenuView, availableWidth, childSpecHeight, 0); rightOfCenter = Math.max(0, rightOfCenter - mMenuView.getMeasuredWidth()); } - boolean showTitle = mTitleLayout != null && mTitleLayout.getVisibility() != GONE && - (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0; - if (showTitle) { - availableWidth = measureChildView(mTitleLayout, availableWidth, childSpecHeight, 0); - leftOfCenter = Math.max(0, leftOfCenter - mTitleLayout.getMeasuredWidth()); - } - - switch (mNavigationMode) { - case ActionBar.NAVIGATION_MODE_LIST: - if (mListNavLayout != null) { - final int itemPaddingSize = showTitle ? mItemPadding * 2 : mItemPadding; - availableWidth = Math.max(0, availableWidth - itemPaddingSize); - leftOfCenter = Math.max(0, leftOfCenter - itemPaddingSize); - mListNavLayout.measure( - MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); - final int listNavWidth = mListNavLayout.getMeasuredWidth(); - availableWidth = Math.max(0, availableWidth - listNavWidth); - leftOfCenter = Math.max(0, leftOfCenter - listNavWidth); + if (mExpandedActionView == null) { + boolean showTitle = mTitleLayout != null && mTitleLayout.getVisibility() != GONE && + (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0; + if (showTitle) { + availableWidth = measureChildView(mTitleLayout, availableWidth, childSpecHeight, 0); + leftOfCenter = Math.max(0, leftOfCenter - mTitleLayout.getMeasuredWidth()); } - break; - case ActionBar.NAVIGATION_MODE_TABS: - if (mTabScrollView != null) { - final int itemPaddingSize = showTitle ? mItemPadding * 2 : mItemPadding; - availableWidth = Math.max(0, availableWidth - itemPaddingSize); - leftOfCenter = Math.max(0, leftOfCenter - itemPaddingSize); - mTabScrollView.measure( - MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); - final int tabWidth = mTabScrollView.getMeasuredWidth(); - availableWidth = Math.max(0, availableWidth - tabWidth); - leftOfCenter = Math.max(0, leftOfCenter - tabWidth); + + switch (mNavigationMode) { + case ActionBar.NAVIGATION_MODE_LIST: + if (mListNavLayout != null) { + final int itemPaddingSize = showTitle ? mItemPadding * 2 : mItemPadding; + availableWidth = Math.max(0, availableWidth - itemPaddingSize); + leftOfCenter = Math.max(0, leftOfCenter - itemPaddingSize); + mListNavLayout.measure( + MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + final int listNavWidth = mListNavLayout.getMeasuredWidth(); + availableWidth = Math.max(0, availableWidth - listNavWidth); + leftOfCenter = Math.max(0, leftOfCenter - listNavWidth); + } + break; + case ActionBar.NAVIGATION_MODE_TABS: + if (mTabScrollView != null) { + final int itemPaddingSize = showTitle ? mItemPadding * 2 : mItemPadding; + availableWidth = Math.max(0, availableWidth - itemPaddingSize); + leftOfCenter = Math.max(0, leftOfCenter - itemPaddingSize); + mTabScrollView.measure( + MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + final int tabWidth = mTabScrollView.getMeasuredWidth(); + availableWidth = Math.max(0, availableWidth - tabWidth); + leftOfCenter = Math.max(0, leftOfCenter - tabWidth); + } + break; } - break; } if (mIndeterminateProgressView != null && @@ -691,8 +775,16 @@ public class ActionBarView extends ViewGroup { rightOfCenter - mIndeterminateProgressView.getMeasuredWidth()); } - if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 && mCustomNavView != null) { - final LayoutParams lp = generateLayoutParams(mCustomNavView.getLayoutParams()); + View customView = null; + if (mExpandedActionView != null) { + customView = mExpandedActionView; + } else if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 && + mCustomNavView != null) { + customView = mCustomNavView; + } + + if (customView != null) { + final LayoutParams lp = generateLayoutParams(customView.getLayoutParams()); final ActionBar.LayoutParams ablp = lp instanceof ActionBar.LayoutParams ? (ActionBar.LayoutParams) lp : null; @@ -729,15 +821,14 @@ public class ActionBarView extends ViewGroup { customNavWidth = Math.min(leftOfCenter, rightOfCenter) * 2; } - mCustomNavView.measure( + customView.measure( MeasureSpec.makeMeasureSpec(customNavWidth, customNavWidthMode), MeasureSpec.makeMeasureSpec(customNavHeight, customNavHeightMode)); } if (mContentHeight <= 0) { int measuredHeight = 0; - final int count = getChildCount(); - for (int i = 0; i < count; i++) { + for (int i = 0; i < childCount; i++) { View v = getChildAt(i); int paddedViewHeight = v.getMeasuredHeight() + verticalPadding; if (paddedViewHeight > measuredHeight) { @@ -760,51 +851,49 @@ public class ActionBarView extends ViewGroup { } } - private int measureChildView(View child, int availableWidth, int childSpecHeight, int spacing) { - child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST), - childSpecHeight); - - availableWidth -= child.getMeasuredWidth(); - availableWidth -= spacing; - - return Math.max(0, availableWidth); - } - @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int x = getPaddingLeft(); final int y = getPaddingTop(); final int contentHeight = b - t - getPaddingTop() - getPaddingBottom(); - if (mHomeLayout.getVisibility() != GONE) { - x += positionChild(mHomeLayout, x, y, contentHeight); + if (contentHeight <= 0) { + // Nothing to do if we can't see anything. + return; } - - final boolean showTitle = mTitleLayout != null && mTitleLayout.getVisibility() != GONE && - (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0; - if (showTitle) { - x += positionChild(mTitleLayout, x, y, contentHeight); - } - - switch (mNavigationMode) { - case ActionBar.NAVIGATION_MODE_STANDARD: - break; - case ActionBar.NAVIGATION_MODE_LIST: - if (mListNavLayout != null) { - if (showTitle) x += mItemPadding; - x += positionChild(mListNavLayout, x, y, contentHeight) + mItemPadding; + + View homeLayout = mExpandedActionView != null ? mExpandedHomeLayout : mHomeLayout; + if (homeLayout.getVisibility() != GONE) { + x += positionChild(homeLayout, x, y, contentHeight); + } + + if (mExpandedActionView == null) { + final boolean showTitle = mTitleLayout != null && mTitleLayout.getVisibility() != GONE && + (mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0; + if (showTitle) { + x += positionChild(mTitleLayout, x, y, contentHeight); } - break; - case ActionBar.NAVIGATION_MODE_TABS: - if (mTabScrollView != null) { - if (showTitle) x += mItemPadding; - x += positionChild(mTabScrollView, x, y, contentHeight) + mItemPadding; + + switch (mNavigationMode) { + case ActionBar.NAVIGATION_MODE_STANDARD: + break; + case ActionBar.NAVIGATION_MODE_LIST: + if (mListNavLayout != null) { + if (showTitle) x += mItemPadding; + x += positionChild(mListNavLayout, x, y, contentHeight) + mItemPadding; + } + break; + case ActionBar.NAVIGATION_MODE_TABS: + if (mTabScrollView != null) { + if (showTitle) x += mItemPadding; + x += positionChild(mTabScrollView, x, y, contentHeight) + mItemPadding; + } + break; } - break; } int menuLeft = r - l - getPaddingRight(); - if (mMenuView != null) { + if (mMenuView != null && mMenuView.getParent() == this) { positionChildInverse(mMenuView, menuLeft, y, contentHeight); menuLeft -= mMenuView.getMeasuredWidth(); } @@ -815,13 +904,20 @@ public class ActionBarView extends ViewGroup { menuLeft -= mIndeterminateProgressView.getMeasuredWidth(); } - if (mCustomNavView != null && (mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) { - LayoutParams lp = mCustomNavView.getLayoutParams(); + View customView = null; + if (mExpandedActionView != null) { + customView = mExpandedActionView; + } else if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 && + mCustomNavView != null) { + customView = mCustomNavView; + } + if (customView != null) { + LayoutParams lp = customView.getLayoutParams(); final ActionBar.LayoutParams ablp = lp instanceof ActionBar.LayoutParams ? (ActionBar.LayoutParams) lp : null; final int gravity = ablp != null ? ablp.gravity : DEFAULT_CUSTOM_GRAVITY; - final int navWidth = mCustomNavView.getMeasuredWidth(); + final int navWidth = customView.getMeasuredWidth(); int topMargin = 0; int bottomMargin = 0; @@ -861,17 +957,17 @@ public class ActionBarView extends ViewGroup { case Gravity.CENTER_VERTICAL: final int paddedTop = mTop + getPaddingTop(); final int paddedBottom = mBottom - getPaddingBottom(); - ypos = ((paddedBottom - paddedTop) - mCustomNavView.getMeasuredHeight()) / 2; + ypos = ((paddedBottom - paddedTop) - customView.getMeasuredHeight()) / 2; break; case Gravity.TOP: ypos = getPaddingTop() + topMargin; break; case Gravity.BOTTOM: - ypos = getHeight() - getPaddingBottom() - mCustomNavView.getMeasuredHeight() + ypos = getHeight() - getPaddingBottom() - customView.getMeasuredHeight() - bottomMargin; break; } - x += positionChild(mCustomNavView, xpos, ypos, contentHeight); + x += positionChild(customView, xpos, ypos, contentHeight); } if (mProgressView != null) { @@ -882,90 +978,83 @@ public class ActionBarView extends ViewGroup { } } - private int positionChild(View child, int x, int y, int contentHeight) { - int childWidth = child.getMeasuredWidth(); - int childHeight = child.getMeasuredHeight(); - int childTop = y + (contentHeight - childHeight) / 2; + @Override + public LayoutParams generateLayoutParams(LayoutParams lp) { + if (lp == null) { + lp = generateDefaultLayoutParams(); + } + return lp; + } - child.layout(x, childTop, x + childWidth, childTop + childHeight); + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState state = new SavedState(superState); - return childWidth; - } - - private int positionChildInverse(View child, int x, int y, int contentHeight) { - int childWidth = child.getMeasuredWidth(); - int childHeight = child.getMeasuredHeight(); - int childTop = y + (contentHeight - childHeight) / 2; + if (mExpandedMenuPresenter != null && mExpandedMenuPresenter.mCurrentExpandedItem != null) { + state.expandedMenuItemId = mExpandedMenuPresenter.mCurrentExpandedItem.getItemId(); + } - child.layout(x - childWidth, childTop, x, childTop + childHeight); + state.isOverflowOpen = isOverflowMenuShowing(); - return childWidth; + return state; } - private static class TabView extends LinearLayout { - private ActionBar.Tab mTab; - - public TabView(Context context, ActionBar.Tab tab) { - super(context, null, com.android.internal.R.attr.actionBarTabStyle); - mTab = tab; + @Override + public void onRestoreInstanceState(Parcelable p) { + SavedState state = (SavedState) p; - final View custom = tab.getCustomView(); - if (custom != null) { - addView(custom); - } else { - // TODO Style tabs based on the theme - - final Drawable icon = tab.getIcon(); - final CharSequence text = tab.getText(); - - if (icon != null) { - ImageView iconView = new ImageView(context); - iconView.setImageDrawable(icon); - LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - lp.gravity = Gravity.CENTER_VERTICAL; - iconView.setLayoutParams(lp); - addView(iconView); - } + super.onRestoreInstanceState(state.getSuperState()); - if (text != null) { - TextView textView = new TextView(context, null, - com.android.internal.R.attr.actionBarTabTextStyle); - textView.setText(text); - textView.setSingleLine(); - textView.setEllipsize(TruncateAt.END); - LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT); - lp.gravity = Gravity.CENTER_VERTICAL; - textView.setLayoutParams(lp); - addView(textView); - } + if (state.expandedMenuItemId != 0 && + mExpandedMenuPresenter != null && mOptionsMenu != null) { + final MenuItem item = mOptionsMenu.findItem(state.expandedMenuItemId); + if (item != null) { + item.expandActionView(); } - - setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.MATCH_PARENT, 1)); } - public ActionBar.Tab getTab() { - return mTab; + if (state.isOverflowOpen) { + postShowOverflowMenu(); } } - private class TabClickListener implements OnClickListener { - public void onClick(View view) { - TabView tabView = (TabView) view; - tabView.getTab().select(); - final int tabCount = mTabLayout.getChildCount(); - for (int i = 0; i < tabCount; i++) { - final View child = mTabLayout.getChildAt(i); - child.setSelected(child == view); - } + static class SavedState extends BaseSavedState { + int expandedMenuItemId; + boolean isOverflowOpen; + + SavedState(Parcelable superState) { + super(superState); } + + private SavedState(Parcel in) { + super(in); + expandedMenuItemId = in.readInt(); + isOverflowOpen = in.readInt() != 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(expandedMenuItemId); + out.writeInt(isOverflowOpen ? 1 : 0); + } + + public static final Parcelable.Creator<SavedState> CREATOR = + new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; } private static class HomeView extends FrameLayout { private View mUpView; - private View mIconView; + private ImageView mIconView; public HomeView(Context context) { this(context, null); @@ -975,25 +1064,22 @@ public class ActionBarView extends ViewGroup { super(context, attrs); } + public void setUp(boolean isUp) { + mUpView.setVisibility(isUp ? VISIBLE : GONE); + } + + public void setIcon(Drawable icon) { + mIconView.setImageDrawable(icon); + } + @Override protected void onFinishInflate() { mUpView = findViewById(com.android.internal.R.id.up); mIconView = (ImageView) findViewById(com.android.internal.R.id.home); } - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - // Make sure we reload positioning elements that may change with configuration. - Resources res = getContext().getResources(); - final int imagePadding = res.getDimensionPixelSize( - com.android.internal.R.dimen.action_bar_home_image_padding); - final int upMargin = res.getDimensionPixelSize( - com.android.internal.R.dimen.action_bar_home_up_margin); - mIconView.setPadding(imagePadding, getPaddingTop(), imagePadding, getPaddingBottom()); - ((LayoutParams) mUpView.getLayoutParams()).rightMargin = upMargin; - mUpView.requestLayout(); + public int getVerticalIconPadding() { + return mIconView.getPaddingTop() + mIconView.getPaddingBottom(); } @Override @@ -1033,4 +1119,111 @@ public class ActionBarView extends ViewGroup { mIconView.layout(iconLeft, iconTop, iconLeft + iconWidth, iconTop + iconHeight); } } + + private class ExpandedActionViewMenuPresenter implements MenuPresenter { + MenuBuilder mMenu; + MenuItemImpl mCurrentExpandedItem; + + @Override + public void initForMenu(Context context, MenuBuilder menu) { + // Clear the expanded action view when menus change. + mExpandedActionView = null; + if (mCurrentExpandedItem != null) { + mCurrentExpandedItem.collapseActionView(); + } + mMenu = menu; + } + + @Override + public MenuView getMenuView(ViewGroup root) { + return null; + } + + @Override + public void updateMenuView(boolean cleared) { + // Make sure the expanded item we have is still there. + if (mCurrentExpandedItem != null) { + boolean found = false; + final int count = mMenu.size(); + for (int i = 0; i < count; i++) { + final MenuItem item = mMenu.getItem(i); + if (item == mCurrentExpandedItem) { + found = true; + break; + } + } + + if (!found) { + // The item we had expanded disappeared. Collapse. + collapseItemActionView(mMenu, mCurrentExpandedItem); + } + } + } + + @Override + public void setCallback(Callback cb) { + } + + @Override + public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + return false; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + } + + @Override + public boolean flagActionItems() { + return false; + } + + @Override + public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { + mExpandedActionView = item.getActionView(); + mExpandedHomeLayout.setIcon(item.getIcon()); + mCurrentExpandedItem = item; + if (mExpandedActionView.getParent() != ActionBarView.this) { + addView(mExpandedActionView); + } + if (mExpandedHomeLayout.getParent() != ActionBarView.this) { + addView(mExpandedHomeLayout); + } + mHomeLayout.setVisibility(GONE); + mTitleLayout.setVisibility(GONE); + if (mTabScrollView != null) mTabScrollView.setVisibility(GONE); + if (mSpinner != null) mSpinner.setVisibility(GONE); + if (mCustomNavView != null) mCustomNavView.setVisibility(GONE); + requestLayout(); + item.setActionViewExpanded(true); + return true; + } + + @Override + public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { + removeView(mExpandedActionView); + removeView(mExpandedHomeLayout); + if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_HOME) != 0) { + mHomeLayout.setVisibility(VISIBLE); + } + if ((mDisplayOptions & ActionBar.DISPLAY_SHOW_TITLE) != 0) { + mTitleLayout.setVisibility(VISIBLE); + } + if (mTabScrollView != null && mNavigationMode == ActionBar.NAVIGATION_MODE_TABS) { + mTabScrollView.setVisibility(VISIBLE); + } + if (mSpinner != null && mNavigationMode == ActionBar.NAVIGATION_MODE_LIST) { + mSpinner.setVisibility(VISIBLE); + } + if (mCustomNavView != null && (mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) { + mCustomNavView.setVisibility(VISIBLE); + } + mExpandedActionView = null; + mExpandedHomeLayout.setIcon(null); + mCurrentExpandedItem = null; + requestLayout(); + item.setActionViewExpanded(false); + return true; + } + } } diff --git a/core/java/com/android/internal/widget/EditableInputConnection.java b/core/java/com/android/internal/widget/EditableInputConnection.java index 9f9f020..32e733b 100644 --- a/core/java/com/android/internal/widget/EditableInputConnection.java +++ b/core/java/com/android/internal/widget/EditableInputConnection.java @@ -18,7 +18,9 @@ package com.android.internal.widget; import android.os.Bundle; import android.text.Editable; +import android.text.Spanned; import android.text.method.KeyListener; +import android.text.style.SuggestionSpan; import android.util.Log; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; @@ -137,6 +139,11 @@ public class EditableInputConnection extends BaseInputConnection { if (mTextView == null) { return super.commitText(text, newCursorPosition); } + if (text instanceof Spanned) { + Spanned spanned = ((Spanned) text); + SuggestionSpan[] spans = spanned.getSpans(0, text.length(), SuggestionSpan.class); + mIMM.registerSuggestionSpansForNotification(spans); + } mTextView.resetErrorChangedFlag(); boolean success = super.commitText(text, newCursorPosition); 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/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index c3f6329..d034eab 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -30,6 +30,7 @@ import android.os.ServiceManager; import android.os.SystemClock; import android.os.storage.IMountService; import android.provider.Settings; +import android.security.KeyStore; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; @@ -49,7 +50,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** - * Utilities for the lock patten and its settings. + * Utilities for the lock pattern and its settings. */ public class LockPatternUtils { @@ -398,12 +399,17 @@ public class LockPatternUtils { } raf.close(); DevicePolicyManager dpm = getDevicePolicyManager(); + KeyStore keyStore = KeyStore.getInstance(); if (pattern != null) { + keyStore.password(patternToString(pattern)); setBoolean(PATTERN_EVER_CHOSEN_KEY, true); setLong(PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_SOMETHING); dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING, pattern .size(), 0, 0, 0, 0, 0, 0); } else { + if (keyStore.isEmpty()) { + keyStore.reset(); + } dpm.setActivePasswordState(DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0); } @@ -486,10 +492,14 @@ public class LockPatternUtils { } raf.close(); DevicePolicyManager dpm = getDevicePolicyManager(); + KeyStore keyStore = KeyStore.getInstance(); if (password != null) { // Update the encryption password. updateEncryptionPassword(password); + // Update the keystore password + keyStore.password(password); + int computedQuality = computePasswordQuality(password); setLong(PASSWORD_TYPE_KEY, Math.max(quality, computedQuality)); if (computedQuality != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) { @@ -542,6 +552,11 @@ public class LockPatternUtils { } setString(PASSWORD_HISTORY_KEY, passwordHistory); } else { + // Conditionally reset the keystore if empty. If + // non-empty, we are just switching key guard type + if (keyStore.isEmpty()) { + keyStore.reset(); + } dpm.setActivePasswordState( DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, 0, 0, 0, 0, 0, 0, 0); } @@ -648,7 +663,7 @@ public class LockPatternUtils { * @param password the gesture pattern. * @return the hash of the pattern in a byte array. */ - public byte[] passwordToHash(String password) { + public byte[] passwordToHash(String password) { if (password == null) { return null; } diff --git a/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java b/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java index 65973b6..3070e3e 100644 --- a/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java +++ b/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java @@ -29,7 +29,7 @@ import android.util.Log; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.View; -import android.view.ViewRoot; +import android.view.ViewAncestor; import com.android.internal.R; public class PasswordEntryKeyboardHelper implements OnKeyboardActionListener { @@ -150,7 +150,7 @@ public class PasswordEntryKeyboardHelper implements OnKeyboardActionListener { KeyEvent event = events[i]; event = KeyEvent.changeFlags(event, event.getFlags() | KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE); - handler.sendMessage(handler.obtainMessage(ViewRoot.DISPATCH_KEY, event)); + handler.sendMessage(handler.obtainMessage(ViewAncestor.DISPATCH_KEY, event)); } } } @@ -158,11 +158,11 @@ public class PasswordEntryKeyboardHelper implements OnKeyboardActionListener { public void sendDownUpKeyEvents(int keyEventCode) { long eventTime = SystemClock.uptimeMillis(); Handler handler = mTargetView.getHandler(); - handler.sendMessage(handler.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + handler.sendMessage(handler.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE))); - handler.sendMessage(handler.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + handler.sendMessage(handler.obtainMessage(ViewAncestor.DISPATCH_KEY_FROM_IME, new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, keyEventCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE))); diff --git a/core/java/com/android/internal/widget/PointerLocationView.java b/core/java/com/android/internal/widget/PointerLocationView.java index 076a1cb..bf1c637 100644 --- a/core/java/com/android/internal/widget/PointerLocationView.java +++ b/core/java/com/android/internal/widget/PointerLocationView.java @@ -320,7 +320,8 @@ public class PointerLocationView extends View { } } - private void logPointerCoords(int action, int index, MotionEvent.PointerCoords coords, int id) { + private void logPointerCoords(int action, int index, MotionEvent.PointerCoords coords, int id, + int toolType, int buttonState) { final String prefix; switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: @@ -357,6 +358,12 @@ public class PointerLocationView extends View { case MotionEvent.ACTION_HOVER_MOVE: prefix = "HOVER MOVE"; break; + case MotionEvent.ACTION_HOVER_ENTER: + prefix = "HOVER ENTER"; + break; + case MotionEvent.ACTION_HOVER_EXIT: + prefix = "HOVER EXIT"; + break; case MotionEvent.ACTION_SCROLL: prefix = "SCROLL"; break; @@ -378,28 +385,19 @@ public class PointerLocationView extends View { .append(" ToolMinor=").append(coords.toolMinor, 3) .append(" Orientation=").append((float)(coords.orientation * 180 / Math.PI), 1) .append("deg") + .append(" Distance=").append(coords.getAxisValue(MotionEvent.AXIS_DISTANCE), 1) .append(" VScroll=").append(coords.getAxisValue(MotionEvent.AXIS_VSCROLL), 1) .append(" HScroll=").append(coords.getAxisValue(MotionEvent.AXIS_HSCROLL), 1) + .append(" ToolType=").append(MotionEvent.toolTypeToString(toolType)) + .append(" ButtonState=").append(MotionEvent.buttonStateToString(buttonState)) .toString()); } public void addPointerEvent(MotionEvent event) { synchronized (mPointers) { - int action = event.getAction(); - - //Log.i(TAG, "Motion: action=0x" + Integer.toHexString(action) - // + " pointers=" + event.getPointerCount()); - + final int action = event.getAction(); int NP = mPointers.size(); - - //mRect.set(0, 0, getWidth(), mHeaderBottom+1); - //invalidate(mRect); - //if (mCurDown) { - // mRect.set(mCurX-mCurWidth-3, mCurY-mCurWidth-3, - // mCurX+mCurWidth+3, mCurY+mCurWidth+3); - //} else { - // mRect.setEmpty(); - //} + if (action == MotionEvent.ACTION_DOWN || (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) { final int index = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) @@ -450,7 +448,8 @@ public class PointerLocationView extends View { final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords; event.getHistoricalPointerCoords(i, historyPos, coords); if (mPrintCoords) { - logPointerCoords(action, i, coords, id); + logPointerCoords(action, i, coords, id, + event.getToolType(i), event.getButtonState()); } if (ps != null) { ps.addTrace(coords.x, coords.y); @@ -463,7 +462,8 @@ public class PointerLocationView extends View { final PointerCoords coords = ps != null ? ps.mCoords : mHoverCoords; event.getPointerCoords(i, coords); if (mPrintCoords) { - logPointerCoords(action, i, coords, id); + logPointerCoords(action, i, coords, id, + event.getToolType(i), event.getButtonState()); } if (ps != null) { ps.addTrace(coords.x, coords.y); @@ -494,12 +494,7 @@ public class PointerLocationView extends View { ps.addTrace(Float.NaN, Float.NaN); } } - - //if (mCurDown) { - // mRect.union(mCurX-mCurWidth-3, mCurY-mCurWidth-3, - // mCurX+mCurWidth+3, mCurY+mCurWidth+3); - //} - //invalidate(mRect); + postInvalidate(); } } diff --git a/core/java/com/android/internal/widget/ScrollingTabContainerView.java b/core/java/com/android/internal/widget/ScrollingTabContainerView.java new file mode 100644 index 0000000..c7d37f2 --- /dev/null +++ b/core/java/com/android/internal/widget/ScrollingTabContainerView.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget; + +import android.app.ActionBar; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.TextUtils.TruncateAt; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class ScrollingTabContainerView extends HorizontalScrollView { + Runnable mTabSelector; + private TabClickListener mTabClickListener; + + private LinearLayout mTabLayout; + + int mMaxTabWidth; + + public ScrollingTabContainerView(Context context) { + super(context); + setHorizontalScrollBarEnabled(false); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + setFillViewport(widthMode == MeasureSpec.EXACTLY); + + final int childCount = getChildCount(); + if (childCount > 1 && + (widthMode == MeasureSpec.EXACTLY || widthMode == MeasureSpec.AT_MOST)) { + if (childCount > 2) { + mMaxTabWidth = (int) (MeasureSpec.getSize(widthMeasureSpec) * 0.4f); + } else { + mMaxTabWidth = MeasureSpec.getSize(widthMeasureSpec) / 2; + } + } else { + mMaxTabWidth = -1; + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + public void setTabSelected(int position) { + if (mTabLayout == null) { + return; + } + + final int tabCount = mTabLayout.getChildCount(); + for (int i = 0; i < tabCount; i++) { + final View child = mTabLayout.getChildAt(i); + final boolean isSelected = i == position; + child.setSelected(isSelected); + if (isSelected) { + animateToTab(position); + } + } + } + + public void animateToTab(int position) { + final View tabView = mTabLayout.getChildAt(position); + if (mTabSelector != null) { + removeCallbacks(mTabSelector); + } + mTabSelector = new Runnable() { + public void run() { + final int scrollPos = tabView.getLeft() - (getWidth() - tabView.getWidth()) / 2; + smoothScrollTo(scrollPos, 0); + mTabSelector = null; + } + }; + post(mTabSelector); + } + + public void setTabLayout(LinearLayout tabLayout) { + if (mTabLayout != tabLayout) { + if (mTabLayout != null) { + ((ViewGroup) mTabLayout.getParent()).removeView(mTabLayout); + } + if (tabLayout != null) { + addView(tabLayout); + } + mTabLayout = tabLayout; + } + } + + public LinearLayout getTabLayout() { + return mTabLayout; + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mTabSelector != null) { + removeCallbacks(mTabSelector); + } + } + + private TabView createTabView(ActionBar.Tab tab) { + final TabView tabView = new TabView(getContext(), tab); + tabView.setFocusable(true); + + if (mTabClickListener == null) { + mTabClickListener = new TabClickListener(); + } + tabView.setOnClickListener(mTabClickListener); + return tabView; + } + + public void addTab(ActionBar.Tab tab, boolean setSelected) { + View tabView = createTabView(tab); + mTabLayout.addView(tabView, new LinearLayout.LayoutParams(0, + LayoutParams.MATCH_PARENT, 1)); + if (setSelected) { + tabView.setSelected(true); + } + } + + public void addTab(ActionBar.Tab tab, int position, boolean setSelected) { + final TabView tabView = createTabView(tab); + mTabLayout.addView(tabView, position, new LinearLayout.LayoutParams( + 0, LayoutParams.MATCH_PARENT, 1)); + if (setSelected) { + tabView.setSelected(true); + } + } + + public void updateTab(int position) { + ((TabView) mTabLayout.getChildAt(position)).update(); + } + + public void removeTabAt(int position) { + if (mTabLayout != null) { + mTabLayout.removeViewAt(position); + } + } + + public void removeAllTabs() { + if (mTabLayout != null) { + mTabLayout.removeAllViews(); + } + } + + private class TabView extends LinearLayout { + private ActionBar.Tab mTab; + private TextView mTextView; + private ImageView mIconView; + private View mCustomView; + + public TabView(Context context, ActionBar.Tab tab) { + super(context, null, com.android.internal.R.attr.actionBarTabStyle); + mTab = tab; + + update(); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Re-measure if we went beyond our maximum size. + if (mMaxTabWidth > 0 && getMeasuredWidth() > mMaxTabWidth) { + super.onMeasure(MeasureSpec.makeMeasureSpec(mMaxTabWidth, MeasureSpec.EXACTLY), + heightMeasureSpec); + } + } + + public void update() { + final ActionBar.Tab tab = mTab; + final View custom = tab.getCustomView(); + if (custom != null) { + addView(custom); + mCustomView = custom; + if (mTextView != null) mTextView.setVisibility(GONE); + if (mIconView != null) { + mIconView.setVisibility(GONE); + mIconView.setImageDrawable(null); + } + } else { + if (mCustomView != null) { + removeView(mCustomView); + mCustomView = null; + } + + final Drawable icon = tab.getIcon(); + final CharSequence text = tab.getText(); + + if (icon != null) { + if (mIconView == null) { + ImageView iconView = new ImageView(getContext()); + LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER_VERTICAL; + iconView.setLayoutParams(lp); + addView(iconView, 0); + mIconView = iconView; + } + mIconView.setImageDrawable(icon); + mIconView.setVisibility(VISIBLE); + } else if (mIconView != null) { + mIconView.setVisibility(GONE); + mIconView.setImageDrawable(null); + } + + if (text != null) { + if (mTextView == null) { + TextView textView = new TextView(getContext(), null, + com.android.internal.R.attr.actionBarTabTextStyle); + textView.setSingleLine(); + textView.setEllipsize(TruncateAt.END); + LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.CENTER_VERTICAL; + textView.setLayoutParams(lp); + addView(textView); + mTextView = textView; + } + mTextView.setText(text); + mTextView.setVisibility(VISIBLE); + } else { + mTextView.setVisibility(GONE); + } + } + } + + public ActionBar.Tab getTab() { + return mTab; + } + } + + private class TabClickListener implements OnClickListener { + public void onClick(View view) { + TabView tabView = (TabView) view; + tabView.getTab().select(); + final int tabCount = mTabLayout.getChildCount(); + for (int i = 0; i < tabCount; i++) { + final View child = mTabLayout.getChildAt(i); + child.setSelected(child == view); + } + } + } +} diff --git a/core/java/com/android/internal/widget/TextProgressBar.java b/core/java/com/android/internal/widget/TextProgressBar.java index aee7b76..e113dd8 100644 --- a/core/java/com/android/internal/widget/TextProgressBar.java +++ b/core/java/com/android/internal/widget/TextProgressBar.java @@ -86,7 +86,8 @@ public class TextProgressBar extends RelativeLayout implements OnChronometerTick // Check if Chronometer should move with with ProgressBar mChronometerFollow = (params.width == ViewGroup.LayoutParams.WRAP_CONTENT); - mChronometerGravity = (mChronometer.getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK); + mChronometerGravity = (mChronometer.getGravity() & + Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK); } else if (childId == PROGRESSBAR_ID && child instanceof ProgressBar) { mProgressBar = (ProgressBar) child; 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; |
