diff options
Diffstat (limited to 'core/java')
218 files changed, 19494 insertions, 2943 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java new file mode 100644 index 0000000..a3456c7 --- /dev/null +++ b/core/java/android/accessibilityservice/AccessibilityService.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.accessibilityservice; + +import com.android.internal.os.HandlerCaller; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; + +/** + * An accessibility service runs in the background and receives callbacks by the system + * when {@link AccessibilityEvent}s are fired. Such events denote some state transition + * in the user interface, for example, the focus has changed, a button has been clicked, + * etc. + * <p> + * An accessibility service extends this class and implements its abstract methods. Such + * a service is declared as any other service in an AndroidManifest.xml but it must also + * specify that it handles the "android.accessibilityservice.AccessibilityService" + * {@link android.content.Intent}. Following is an example of such a declaration: + * <p> + * <code> + * <service android:name=".MyAccessibilityService"><br> + * <intent-filter><br> + * <action android:name="android.accessibilityservice.AccessibilityService" /><br> + * </intent-filter><br> + * </service><br> + * </code> + * <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()}. + * <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 + * of interest has been fired. + * <p> + * <b>Notification strategy</b> + * <p> + * For each feedback type only one accessibility service is notified. Services are notified + * in the order of registration. Hence, if two services are registered for the same + * feedback type in the same package the first one wins. It is possible however, to + * register a service as the default one for a given feedback type. In such a case this + * service is invoked if no other service was interested in the event. In other words, default + * services do not compete with other services and are notified last regardless of the + * registration order. This enables "generic" accessibility services that work reasonably + * well with most applications to coexist with "polished" ones that are targeted for + * specific applications. + * <p> + * <b>Event types</b> + * <p> + * {@link AccessibilityEvent#TYPE_VIEW_CLICKED} + * {@link AccessibilityEvent#TYPE_VIEW_LONG_CLICKED} + * {@link AccessibilityEvent#TYPE_VIEW_FOCUSED} + * {@link AccessibilityEvent#TYPE_VIEW_SELECTED} + * {@link AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED} + * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} + * {@link AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED} + * <p> + * <b>Feedback types</b> + * <p> + * {@link AccessibilityServiceInfo#FEEDBACK_AUDIBLE} + * {@link AccessibilityServiceInfo#FEEDBACK_HAPTIC} + * {@link AccessibilityServiceInfo#FEEDBACK_AUDIBLE} + * {@link AccessibilityServiceInfo#FEEDBACK_VISUAL} + * {@link AccessibilityServiceInfo#FEEDBACK_GENERIC} + * + * @see AccessibilityEvent + * @see AccessibilityServiceInfo + * @see android.view.accessibility.AccessibilityManager + * + * 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 + * settled down. + */ +public abstract class AccessibilityService extends Service { + /** + * The {@link Intent} that must be declared as handled by the service. + */ + public static final String SERVICE_INTERFACE = + "android.accessibilityservice.AccessibilityService"; + + private static final String LOG_TAG = "AccessibilityService"; + + private AccessibilityServiceInfo mInfo; + + IAccessibilityServiceConnection mConnection; + + /** + * Callback for {@link android.view.accessibility.AccessibilityEvent}s. + * + * @param event An event. + */ + public abstract void onAccessibilityEvent(AccessibilityEvent event); + + /** + * Callback for interrupting the accessibility feedback. + */ + public abstract void onInterrupt(); + + /** + * This method is a part of the {@link AccessibilityService} lifecycle and is + * called after the system has successfully bound to the service. If is + * convenient to use this method for setting the {@link AccessibilityServiceInfo}. + * + * @see AccessibilityServiceInfo + * @see #setServiceInfo(AccessibilityServiceInfo) + */ + protected void onServiceConnected() { + + } + + /** + * Sets the {@link AccessibilityServiceInfo} that describes this service. + * <p> + * Note: You can call this method any time but the info will be picked up after + * the system has bound to this service and when this method is called thereafter. + * + * @param info The info. + */ + public final void setServiceInfo(AccessibilityServiceInfo info) { + mInfo = info; + sendServiceInfo(); + } + + /** + * Sets the {@link AccessibilityServiceInfo} for this service if the latter is + * properly set and there is an {@link IAccessibilityServiceConnection} to the + * AccessibilityManagerService. + */ + private void sendServiceInfo() { + if (mInfo != null && mConnection != null) { + try { + mConnection.setServiceInfo(mInfo); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error while setting AccessibilityServiceInfo", re); + } + } + } + + @Override + public final IBinder onBind(Intent intent) { + return new IEventListenerWrapper(this); + } + + /** + * Implements the internal {@link IEventListener} interface to convert + * incoming calls to it back to calls on an {@link AccessibilityService}. + */ + class IEventListenerWrapper extends IEventListener.Stub + implements HandlerCaller.Callback { + + private static final int DO_SET_SET_CONNECTION = 10; + private static final int DO_ON_INTERRUPT = 20; + private static final int DO_ON_ACCESSIBILITY_EVENT = 30; + + private final HandlerCaller mCaller; + + private AccessibilityService mTarget; + + public IEventListenerWrapper(AccessibilityService context) { + mTarget = context; + mCaller = new HandlerCaller(context, this); + } + + public void setConnection(IAccessibilityServiceConnection connection) { + Message message = mCaller.obtainMessageO(DO_SET_SET_CONNECTION, connection); + mCaller.sendMessage(message); + } + + public void onInterrupt() { + Message message = mCaller.obtainMessage(DO_ON_INTERRUPT); + mCaller.sendMessage(message); + } + + public void onAccessibilityEvent(AccessibilityEvent event) { + Message message = mCaller.obtainMessageO(DO_ON_ACCESSIBILITY_EVENT, event); + mCaller.sendMessage(message); + } + + public void executeMessage(Message message) { + switch (message.what) { + case DO_ON_ACCESSIBILITY_EVENT : + AccessibilityEvent event = (AccessibilityEvent) message.obj; + mTarget.onAccessibilityEvent(event); + event.recycle(); + return; + case DO_ON_INTERRUPT : + mTarget.onInterrupt(); + return; + case DO_SET_SET_CONNECTION : + mConnection = ((IAccessibilityServiceConnection) message.obj); + mTarget.onServiceConnected(); + return; + default : + Log.w(LOG_TAG, "Unknown message type " + message.what); + } + } + } +} diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.aidl b/core/java/android/accessibilityservice/AccessibilityServiceInfo.aidl new file mode 100644 index 0000000..1f5d385 --- /dev/null +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.aidl @@ -0,0 +1,19 @@ +/* + * 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.accessibilityservice; + +parcelable AccessibilityServiceInfo; diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java new file mode 100644 index 0000000..4761f98 --- /dev/null +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -0,0 +1,145 @@ +/* + * 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.accessibilityservice; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * This class describes an {@link AccessibilityService}. The system + * notifies an {@link AccessibilityService} for + * {@link android.view.accessibility.AccessibilityEvent}s + * according to the information encapsulated in this class. + * + * @see AccessibilityService + * @see android.view.accessibility.AccessibilityEvent + */ +public class AccessibilityServiceInfo implements Parcelable { + + /** + * Denotes spoken feedback. + */ + public static final int FEEDBACK_SPOKEN = 0x0000001; + + /** + * Denotes haptic feedback. + */ + public static final int FEEDBACK_HAPTIC = 0x0000002; + + /** + * Denotes audible (not spoken) feedback. + */ + public static final int FEEDBACK_AUDIBLE = 0x0000004; + + /** + * Denotes visual feedback. + */ + public static final int FEEDBACK_VISUAL = 0x0000008; + + /** + * Denotes generic feedback. + */ + public static final int FEEDBACK_GENERIC = 0x0000010; + + /** + * If an {@link AccessibilityService} is the default for a given type. + * Default service is invoked only if no package specific one exists. In case of + * more than one package specific service only the earlier registered is notified. + */ + public static final int DEFAULT = 0x0000001; + + /** + * The event types an {@link AccessibilityService} is interested in. + * + * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_CLICKED + * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_FOCUSED + * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_SELECTED + * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED + * @see android.view.accessibility.AccessibilityEvent#TYPE_ACTIVITY_STARTED + * @see android.view.accessibility.AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED + * @see android.view.accessibility.AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED + */ + public int eventTypes; + + /** + * The package names an {@link AccessibilityService} is interested in. Setting + * to null is equivalent to all packages. + */ + public String[] packageNames; + + /** + * The feedback type an {@link AccessibilityService} provides. + * + * @see #FEEDBACK_AUDIBLE + * @see #FEEDBACK_GENERIC + * @see #FEEDBACK_HAPTIC + * @see #FEEDBACK_SPOKEN + * @see #FEEDBACK_VISUAL + */ + public int feedbackType; + + /** + * The timeout after the most recent event of a given type before an + * {@link AccessibilityService} is notified. + * <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 + * settled down + */ + public long notificationTimeout; + + /** + * This field represents a set of flags used for configuring an + * {@link AccessibilityService}. + * + * @see #DEFAULT + */ + public int flags; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(eventTypes); + parcel.writeStringArray(packageNames); + parcel.writeInt(feedbackType); + parcel.writeLong(notificationTimeout); + parcel.writeInt(flags); + } + + /** + * @see Parcelable.Creator + */ + public static final Parcelable.Creator<AccessibilityServiceInfo> CREATOR = + 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(); + return info; + } + + public AccessibilityServiceInfo[] newArray(int size) { + return new AccessibilityServiceInfo[size]; + } + }; +} diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl new file mode 100644 index 0000000..7157def --- /dev/null +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -0,0 +1,30 @@ +/* + * 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.accessibilityservice; + +import android.accessibilityservice.AccessibilityServiceInfo; + +/** + * Interface AccessibilityManagerService#Service implements, and passes to an + * AccessibilityService so it can dynamically configure how the system handles it. + * + * @hide + */ +oneway interface IAccessibilityServiceConnection { + + void setServiceInfo(in AccessibilityServiceInfo info); +} diff --git a/core/java/android/accessibilityservice/IEventListener.aidl b/core/java/android/accessibilityservice/IEventListener.aidl new file mode 100644 index 0000000..5b849f1 --- /dev/null +++ b/core/java/android/accessibilityservice/IEventListener.aidl @@ -0,0 +1,34 @@ +/* +** Copyright 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.accessibilityservice; + +import android.accessibilityservice.IAccessibilityServiceConnection; +import android.view.accessibility.AccessibilityEvent; + +/** + * Top-level interface to accessibility service component (implemented in Service). + * + * @hide + */ + oneway interface IEventListener { + + void setConnection(in IAccessibilityServiceConnection connection); + + void onAccessibilityEvent(in AccessibilityEvent event); + + void onInterrupt(); +} diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 9b1f0f9..ca9632a 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -16,11 +16,14 @@ package android.app; +import com.android.internal.policy.PolicyManager; + import android.content.ComponentCallbacks; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.IIntentSender; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.res.Configuration; @@ -32,11 +35,12 @@ import android.graphics.drawable.Drawable; import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; -import android.os.RemoteException; import android.os.Handler; import android.os.IBinder; +import android.os.RemoteException; import android.text.Selection; import android.text.SpannableStringBuilder; +import android.text.TextUtils; import android.text.method.TextKeyListener; import android.util.AttributeSet; import android.util.Config; @@ -58,10 +62,10 @@ import android.view.Window; import android.view.WindowManager; import android.view.ContextMenu.ContextMenuInfo; import android.view.View.OnCreateContextMenuListener; +import android.view.ViewGroup.LayoutParams; +import android.view.accessibility.AccessibilityEvent; import android.widget.AdapterView; -import com.android.internal.policy.PolicyManager; - import java.util.ArrayList; import java.util.HashMap; @@ -625,6 +629,8 @@ public class Activity extends ContextThemeWrapper boolean mStartedActivity; /*package*/ int mConfigChangeFlags; /*package*/ Configuration mCurrentConfig; + private SearchManager mSearchManager; + private Bundle mSearchDialogState = null; private Window mWindow; @@ -785,6 +791,9 @@ public class Activity extends ContextThemeWrapper protected void onCreate(Bundle savedInstanceState) { mVisibleFromClient = mWindow.getWindowStyle().getBoolean( com.android.internal.R.styleable.Window_windowNoDisplay, true); + // uses super.getSystemService() since this.getSystemService() looks at the + // mSearchManager field. + mSearchManager = (SearchManager) super.getSystemService(Context.SEARCH_SERVICE); mCalled = true; } @@ -802,9 +811,10 @@ public class Activity extends ContextThemeWrapper // Also restore the state of a search dialog (if any) // TODO more generic than just this manager - SearchManager searchManager = - (SearchManager) getSystemService(Context.SEARCH_SERVICE); - searchManager.restoreSearchDialog(savedInstanceState, SAVED_SEARCH_DIALOG_KEY); + Bundle searchState = savedInstanceState.getBundle(SAVED_SEARCH_DIALOG_KEY); + if (searchState != null) { + mSearchManager.restoreSearchDialog(searchState); + } } /** @@ -854,13 +864,26 @@ public class Activity extends ContextThemeWrapper final Integer dialogId = ids[i]; Bundle dialogState = b.getBundle(savedDialogKeyFor(dialogId)); if (dialogState != null) { - final Dialog dialog = onCreateDialog(dialogId); - dialog.onRestoreInstanceState(dialogState); + // Calling onRestoreInstanceState() below will invoke dispatchOnCreate + // so tell createDialog() not to do it, otherwise we get an exception + final Dialog dialog = createDialog(dialogId, false); mManagedDialogs.put(dialogId, dialog); + onPrepareDialog(dialogId, dialog); + dialog.onRestoreInstanceState(dialogState); } } } + private Dialog createDialog(Integer dialogId, boolean dispatchOnCreate) { + final Dialog dialog = onCreateDialog(dialogId); + if (dialog == null) { + throw new IllegalArgumentException("Activity#onCreateDialog did " + + "not create a dialog for id " + dialogId); + } + if (dispatchOnCreate) dialog.dispatchOnCreate(null); + return dialog; + } + private String savedDialogKeyFor(int key) { return SAVED_DIALOG_KEY_PREFIX + key; } @@ -1010,9 +1033,11 @@ public class Activity extends ContextThemeWrapper // Also save the state of a search dialog (if any) // TODO more generic than just this manager - SearchManager searchManager = - (SearchManager) getSystemService(Context.SEARCH_SERVICE); - searchManager.saveSearchDialog(outState, SAVED_SEARCH_DIALOG_KEY); + // onPause() should always be called before this method, so mSearchManagerState + // should be up to date. + if (mSearchDialogState != null) { + outState.putBundle(SAVED_SEARCH_DIALOG_KEY, mSearchDialogState); + } } /** @@ -1283,12 +1308,6 @@ public class Activity extends ContextThemeWrapper } } } - - // also dismiss search dialog if showing - // TODO more generic than just this manager - SearchManager searchManager = - (SearchManager) getSystemService(Context.SEARCH_SERVICE); - searchManager.stopSearch(); // close any cursors we are managing. int numCursors = mManagedCursors.size(); @@ -1298,6 +1317,10 @@ public class Activity extends ContextThemeWrapper c.mCursor.close(); } } + + // Clear any search state saved in performPause(). If the state may be needed in the + // future, it will have been saved by performSaveInstanceState() + mSearchDialogState = null; } /** @@ -1321,9 +1344,7 @@ public class Activity extends ContextThemeWrapper // also update search dialog if showing // TODO more generic than just this manager - SearchManager searchManager = - (SearchManager) getSystemService(Context.SEARCH_SERVICE); - searchManager.onConfigurationChanged(newConfig); + mSearchManager.onConfigurationChanged(newConfig); if (mWindow != null) { // Pass the configuration changed event to the window @@ -2013,7 +2034,24 @@ public class Activity extends ContextThemeWrapper } return onTrackballEvent(ev); } - + + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(getClass().getName()); + event.setPackageName(getPackageName()); + + LayoutParams params = getWindow().getAttributes(); + boolean isFullScreen = (params.width == LayoutParams.FILL_PARENT) && + (params.height == LayoutParams.FILL_PARENT); + event.setFullScreen(isFullScreen); + + CharSequence title = getTitle(); + if (!TextUtils.isEmpty(title)) { + event.getText().add(title); + } + + return true; + } + /** * Default implementation of * {@link android.view.Window.Callback#onCreatePanelView} @@ -2394,12 +2432,7 @@ public class Activity extends ContextThemeWrapper } Dialog dialog = mManagedDialogs.get(id); if (dialog == null) { - dialog = onCreateDialog(id); - if (dialog == null) { - throw new IllegalArgumentException("Activity#onCreateDialog did " - + "not create a dialog for id " + id); - } - dialog.dispatchOnCreate(null); + dialog = createDialog(id, true); mManagedDialogs.put(id, dialog); } @@ -2523,10 +2556,7 @@ public class Activity extends ContextThemeWrapper */ public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch) { - // activate the search manager and start it up! - SearchManager searchManager = (SearchManager) - getSystemService(Context.SEARCH_SERVICE); - searchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(), + mSearchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(), appSearchData, globalSearch); } @@ -3245,6 +3275,8 @@ public class Activity extends ContextThemeWrapper if (WINDOW_SERVICE.equals(name)) { return mWindowManager; + } else if (SEARCH_SERVICE.equals(name)) { + return mSearchManager; } return super.getSystemService(name); } @@ -3543,10 +3575,21 @@ public class Activity extends ContextThemeWrapper "Activity " + mComponent.toShortString() + " did not call through to super.onPostResume()"); } + + // restore search dialog, if any + if (mSearchDialogState != null) { + mSearchManager.restoreSearchDialog(mSearchDialogState); + } + mSearchDialogState = null; } final void performPause() { onPause(); + + // save search dialog state if the search dialog is open, + // and then dismiss the search dialog + mSearchDialogState = mSearchManager.saveSearchDialog(); + mSearchManager.stopSearch(); } final void performUserLeaving() { diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index 541f413..dfa8139 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -17,9 +17,11 @@ package android.app; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Intent; import android.content.IntentFilter; +import android.content.IIntentSender; +import android.content.IIntentReceiver; +import android.content.pm.ApplicationInfo; import android.content.pm.ConfigurationInfo; import android.content.pm.IPackageDataObserver; import android.content.res.Configuration; @@ -984,7 +986,9 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM String process = data.readString(); boolean start = data.readInt() != 0; String path = data.readString(); - boolean res = profileControl(process, start, path); + ParcelFileDescriptor fd = data.readInt() != 0 + ? data.readFileDescriptor() : null; + boolean res = profileControl(process, start, path, fd); reply.writeNoException(); reply.writeInt(res ? 1 : 0); return true; @@ -998,6 +1002,20 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case STOP_APP_SWITCHES_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + stopAppSwitches(); + reply.writeNoException(); + return true; + } + + case RESUME_APP_SWITCHES_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + resumeAppSwitches(); + reply.writeNoException(); + return true; + } + case PEEK_SERVICE_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); Intent service = Intent.CREATOR.createFromParcel(data); @@ -1007,6 +1025,33 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM reply.writeStrongBinder(binder); return true; } + + case START_BACKUP_AGENT_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + ApplicationInfo info = ApplicationInfo.CREATOR.createFromParcel(data); + int backupRestoreMode = data.readInt(); + boolean success = bindBackupAgent(info, backupRestoreMode); + reply.writeNoException(); + reply.writeInt(success ? 1 : 0); + return true; + } + + case BACKUP_AGENT_CREATED_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + String packageName = data.readString(); + IBinder agent = data.readStrongBinder(); + backupAgentCreated(packageName, agent); + reply.writeNoException(); + return true; + } + + case UNBIND_BACKUP_AGENT_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + ApplicationInfo info = ApplicationInfo.CREATOR.createFromParcel(data); + unbindBackupAgent(info); + reply.writeNoException(); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -1667,6 +1712,43 @@ class ActivityManagerProxy implements IActivityManager return binder; } + public boolean bindBackupAgent(ApplicationInfo app, int backupRestoreMode) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + app.writeToParcel(data, 0); + data.writeInt(backupRestoreMode); + mRemote.transact(START_BACKUP_AGENT_TRANSACTION, data, reply, 0); + reply.readException(); + boolean success = reply.readInt() != 0; + reply.recycle(); + data.recycle(); + return success; + } + + public void backupAgentCreated(String packageName, IBinder agent) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeString(packageName); + data.writeStrongBinder(agent); + mRemote.transact(BACKUP_AGENT_CREATED_TRANSACTION, data, reply, 0); + reply.recycle(); + data.recycle(); + } + + public void unbindBackupAgent(ApplicationInfo app) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + app.writeToParcel(data, 0); + mRemote.transact(UNBIND_BACKUP_AGENT_TRANSACTION, data, reply, 0); + reply.readException(); + reply.recycle(); + data.recycle(); + } + public boolean startInstrumentation(ComponentName className, String profileFile, int flags, Bundle arguments, IInstrumentationWatcher watcher) throws RemoteException { @@ -2152,7 +2234,7 @@ class ActivityManagerProxy implements IActivityManager } public boolean profileControl(String process, boolean start, - String path) throws RemoteException + String path, ParcelFileDescriptor fd) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -2160,6 +2242,12 @@ class ActivityManagerProxy implements IActivityManager data.writeString(process); data.writeInt(start ? 1 : 0); data.writeString(path); + if (fd != null) { + data.writeInt(1); + fd.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + data.writeInt(0); + } mRemote.transact(PROFILE_CONTROL_TRANSACTION, data, reply, 0); reply.readException(); boolean res = reply.readInt() != 0; @@ -2182,5 +2270,25 @@ class ActivityManagerProxy implements IActivityManager return res; } + public void stopAppSwitches() throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + mRemote.transact(STOP_APP_SWITCHES_TRANSACTION, data, reply, 0); + reply.readException(); + reply.recycle(); + data.recycle(); + } + + public void resumeAppSwitches() throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + mRemote.transact(RESUME_APP_SWITCHES_TRANSACTION, data, reply, 0); + reply.readException(); + reply.recycle(); + data.recycle(); + } + private IBinder mRemote; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 1e15d14..5ee29ac 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -23,6 +23,7 @@ import android.content.ContentProvider; import android.content.Context; import android.content.IContentProvider; import android.content.Intent; +import android.content.IIntentReceiver; import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; @@ -31,6 +32,7 @@ import android.content.pm.InstrumentationInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; +import android.content.pm.PackageParser.Component; import android.content.res.AssetManager; import android.content.res.Configuration; import android.content.res.Resources; @@ -46,6 +48,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; +import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; @@ -72,6 +75,7 @@ import org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; +import java.io.IOException; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -115,6 +119,7 @@ public final class ActivityThread { private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; private static final boolean DEBUG_BROADCAST = false; private static final boolean DEBUG_RESULTS = false; + private static final boolean DEBUG_BACKUP = true; private static final long MIN_TIME_BETWEEN_GCS = 5*1000; private static final Pattern PATTERN_SEMICOLON = Pattern.compile(";"); private static final int SQLITE_MEM_RELEASED_EVENT_LOG_TAG = 75003; @@ -161,7 +166,7 @@ public final class ActivityThread { return metrics; } - Resources getTopLevelResources(String appDir, float applicationScale) { + Resources getTopLevelResources(String appDir, PackageInfo pkgInfo) { synchronized (mPackages) { //Log.w(TAG, "getTopLevelResources: " + appDir); WeakReference<Resources> wr = mActiveResources.get(appDir); @@ -180,23 +185,17 @@ public final class ActivityThread { if (assets.addAssetPath(appDir) == 0) { return null; } - DisplayMetrics metrics = getDisplayMetricsLocked(false); - // density used to load resources - // scaledDensity is calculated in Resources constructor - // - boolean usePreloaded = true; - - // TODO: use explicit flag to indicate the compatibility mode. - if (applicationScale != 1.0f) { - usePreloaded = false; - DisplayMetrics newMetrics = new DisplayMetrics(); - newMetrics.setTo(metrics); - float newDensity = metrics.density / applicationScale; - newMetrics.updateDensity(newDensity); - metrics = newMetrics; + ApplicationInfo appInfo; + try { + appInfo = getPackageManager().getApplicationInfo( + pkgInfo.getPackageName(), + PackageManager.GET_SUPPORTS_DENSITIES); + } catch (RemoteException e) { + throw new AssertionError(e); } //Log.i(TAG, "Resource:" + appDir + ", display metrics=" + metrics); - r = new Resources(assets, metrics, getConfiguration(), usePreloaded); + DisplayMetrics metrics = getDisplayMetricsLocked(false); + r = new Resources(assets, metrics, getConfiguration(), appInfo); //Log.i(TAG, "Created app resources " + r + ": " + r.getConfiguration()); // XXX need to remove entries when weak references go away mActiveResources.put(appDir, new WeakReference<Resources>(r)); @@ -224,7 +223,6 @@ public final class ActivityThread { private Resources mResources; private ClassLoader mClassLoader; private Application mApplication; - private float mApplicationScale; private final HashMap<Context, HashMap<BroadcastReceiver, ReceiverDispatcher>> mReceivers = new HashMap<Context, HashMap<BroadcastReceiver, ReceiverDispatcher>>(); @@ -267,8 +265,6 @@ public final class ActivityThread { mClassLoader = mSystemContext.getClassLoader(); mResources = mSystemContext.getResources(); } - - mApplicationScale = -1.0f; } public PackageInfo(ActivityThread activityThread, String name, @@ -287,56 +283,20 @@ public final class ActivityThread { mIncludeCode = true; mClassLoader = systemContext.getClassLoader(); mResources = systemContext.getResources(); - mApplicationScale = systemContext.getApplicationScale(); } public String getPackageName() { return mPackageName; } + public ApplicationInfo getApplicationInfo() { + return mApplicationInfo; + } + public boolean isSecurityViolation() { return mSecurityViolation; } - public float getApplicationScale() { - if (mApplicationScale > 0.0f) { - return mApplicationScale; - } - DisplayMetrics metrics = mActivityThread.getDisplayMetricsLocked(false); - // Find out the density scale (relative to 160) of the supported density that - // is closest to the system's density. - try { - ApplicationInfo ai = getPackageManager().getApplicationInfo( - mPackageName, PackageManager.GET_SUPPORTS_DENSITIES); - - float appScale = -1.0f; - if (ai.supportsDensities != null) { - int minDiff = Integer.MAX_VALUE; - for (int density : ai.supportsDensities) { - int tmpDiff = (int) Math.abs(DisplayMetrics.DEVICE_DENSITY - density); - if (tmpDiff == 0) { - appScale = 1.0f; - break; - } - // prefer higher density (appScale>1.0), unless that's only option. - if (tmpDiff < minDiff && appScale < 1.0f) { - appScale = DisplayMetrics.DEVICE_DENSITY / density; - minDiff = tmpDiff; - } - } - } - if (appScale < 0.0f) { - mApplicationScale = metrics.density; - } else { - mApplicationScale = appScale; - } - } catch (RemoteException e) { - throw new AssertionError(e); - } - if (localLOGV) Log.v(TAG, "appScale=" + mApplicationScale + ", pkg=" + mPackageName); - return mApplicationScale; - } - /** * Gets the array of shared libraries that are listed as * used by the given package. @@ -494,12 +454,12 @@ public final class ActivityThread { public Resources getResources(ActivityThread mainThread) { if (mResources == null) { - mResources = mainThread.getTopLevelResources(mResDir, getApplicationScale()); + mResources = mainThread.getTopLevelResources(mResDir, this); } return mResources; } - public Application makeApplication() { + public Application makeApplication(boolean forceDefaultAppClass) { if (mApplication != null) { return mApplication; } @@ -507,7 +467,7 @@ public final class ActivityThread { Application app = null; String appClass = mApplicationInfo.className; - if (appClass == null) { + if (forceDefaultAppClass || (appClass == null)) { appClass = "android.app.Application"; } @@ -1199,6 +1159,16 @@ public final class ActivityThread { } } + private static final class CreateBackupAgentData { + ApplicationInfo appInfo; + int backupMode; + public String toString() { + return "CreateBackupAgentData{appInfo=" + appInfo + + " backupAgent=" + appInfo.backupAgentName + + " mode=" + backupMode + "}"; + } + } + private static final class CreateServiceData { IBinder token; ServiceInfo info; @@ -1239,6 +1209,7 @@ public final class ActivityThread { Bundle instrumentationArgs; IInstrumentationWatcher instrumentationWatcher; int debugMode; + boolean restrictedBackupMode; Configuration config; boolean handlingProfiling; public String toString() { @@ -1267,6 +1238,11 @@ public final class ActivityThread { String who; } + private static final class ProfilerControlData { + String path; + ParcelFileDescriptor fd; + } + private final class ApplicationThread extends ApplicationThreadNative { private static final String HEAP_COLUMN = "%17s %8s %8s %8s %8s"; private static final String ONE_COUNT_COLUMN = "%17s %8d"; @@ -1374,6 +1350,21 @@ public final class ActivityThread { queueOrSendMessage(H.RECEIVER, r); } + public final void scheduleCreateBackupAgent(ApplicationInfo app, int backupMode) { + CreateBackupAgentData d = new CreateBackupAgentData(); + d.appInfo = app; + d.backupMode = backupMode; + + queueOrSendMessage(H.CREATE_BACKUP_AGENT, d); + } + + public final void scheduleDestroyBackupAgent(ApplicationInfo app) { + CreateBackupAgentData d = new CreateBackupAgentData(); + d.appInfo = app; + + queueOrSendMessage(H.DESTROY_BACKUP_AGENT, d); + } + public final void scheduleCreateService(IBinder token, ServiceInfo info) { CreateServiceData s = new CreateServiceData(); @@ -1419,7 +1410,7 @@ public final class ActivityThread { ApplicationInfo appInfo, List<ProviderInfo> providers, ComponentName instrumentationName, String profileFile, Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher, - int debugMode, Configuration config, + int debugMode, boolean isRestrictedBackupMode, Configuration config, Map<String, IBinder> services) { Process.setArgV0(processName); @@ -1437,6 +1428,7 @@ public final class ActivityThread { data.instrumentationArgs = instrumentationArgs; data.instrumentationWatcher = instrumentationWatcher; data.debugMode = debugMode; + data.restrictedBackupMode = isRestrictedBackupMode; data.config = config; queueOrSendMessage(H.BIND_APPLICATION, data); } @@ -1509,10 +1501,25 @@ public final class ActivityThread { } } - public void profilerControl(boolean start, String path) { - queueOrSendMessage(H.PROFILER_CONTROL, path, start ? 1 : 0); + public void profilerControl(boolean start, String path, ParcelFileDescriptor fd) { + ProfilerControlData pcd = new ProfilerControlData(); + pcd.path = path; + pcd.fd = fd; + queueOrSendMessage(H.PROFILER_CONTROL, pcd, start ? 1 : 0); + } + + public void setSchedulingGroup(int group) { + // Note: do this immediately, since going into the foreground + // should happen regardless of what pending work we have to do + // and the activity manager will wait for us to report back that + // we are done before sending us to the background. + try { + Process.setProcessGroup(Process.myPid(), group); + } catch (Exception e) { + Log.w(TAG, "Failed setting process group to " + group, e); + } } - + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { long nativeMax = Debug.getNativeHeapSize() / 1024; @@ -1706,6 +1713,8 @@ public final class ActivityThread { public static final int ACTIVITY_CONFIGURATION_CHANGED = 125; public static final int RELAUNCH_ACTIVITY = 126; public static final int PROFILER_CONTROL = 127; + public static final int CREATE_BACKUP_AGENT = 128; + public static final int DESTROY_BACKUP_AGENT = 129; String codeToString(int code) { if (localLOGV) { switch (code) { @@ -1737,6 +1746,8 @@ public final class ActivityThread { case ACTIVITY_CONFIGURATION_CHANGED: return "ACTIVITY_CONFIGURATION_CHANGED"; case RELAUNCH_ACTIVITY: return "RELAUNCH_ACTIVITY"; case PROFILER_CONTROL: return "PROFILER_CONTROL"; + case CREATE_BACKUP_AGENT: return "CREATE_BACKUP_AGENT"; + case DESTROY_BACKUP_AGENT: return "DESTROY_BACKUP_AGENT"; } } return "(unknown)"; @@ -1837,7 +1848,13 @@ public final class ActivityThread { handleActivityConfigurationChanged((IBinder)msg.obj); break; case PROFILER_CONTROL: - handleProfilerControl(msg.arg1 != 0, (String)msg.obj); + handleProfilerControl(msg.arg1 != 0, (ProfilerControlData)msg.obj); + break; + case CREATE_BACKUP_AGENT: + handleCreateBackupAgent((CreateBackupAgentData)msg.obj); + break; + case DESTROY_BACKUP_AGENT: + handleDestroyBackupAgent((CreateBackupAgentData)msg.obj); break; } } @@ -1896,6 +1913,8 @@ public final class ActivityThread { Application mInitialApplication; final ArrayList<Application> mAllApplications = new ArrayList<Application>(); + // set of instantiated backup agents, keyed by package name + final HashMap<String, BackupAgent> mBackupAgents = new HashMap<String, BackupAgent>(); static final ThreadLocal sThreadLocal = new ThreadLocal(); Instrumentation mInstrumentation; String mInstrumentationAppDir = null; @@ -2079,6 +2098,10 @@ public final class ActivityThread { return mInitialApplication; } + public String getProcessName() { + return mBoundApplication.processName; + } + public ApplicationContext getSystemContext() { synchronized (this) { if (mSystemContext == null) { @@ -2257,7 +2280,7 @@ public final class ActivityThread { } try { - Application app = r.packageInfo.makeApplication(); + Application app = r.packageInfo.makeApplication(false); if (localLOGV) Log.v(TAG, "Performing launch of " + r); if (localLOGV) Log.v( @@ -2452,7 +2475,7 @@ public final class ActivityThread { } try { - Application app = packageInfo.makeApplication(); + Application app = packageInfo.makeApplication(false); if (localLOGV) Log.v( TAG, "Performing receive of " + data.intent @@ -2495,6 +2518,85 @@ public final class ActivityThread { } } + // Instantiate a BackupAgent and tell it that it's alive + private final void handleCreateBackupAgent(CreateBackupAgentData data) { + if (DEBUG_BACKUP) Log.v(TAG, "handleCreateBackupAgent: " + data); + + // no longer idle; we have backup work to do + unscheduleGcIdler(); + + // instantiate the BackupAgent class named in the manifest + PackageInfo packageInfo = getPackageInfoNoCheck(data.appInfo); + String packageName = packageInfo.mPackageName; + if (mBackupAgents.get(packageName) != null) { + Log.d(TAG, "BackupAgent " + " for " + packageName + + " already exists"); + return; + } + + BackupAgent agent = null; + String classname = data.appInfo.backupAgentName; + if (classname == null) { + if (data.backupMode == IApplicationThread.BACKUP_MODE_INCREMENTAL) { + Log.e(TAG, "Attempted incremental backup but no defined agent for " + + packageName); + return; + } + classname = "android.app.FullBackupAgent"; + } + try { + java.lang.ClassLoader cl = packageInfo.getClassLoader(); + agent = (BackupAgent) cl.loadClass(data.appInfo.backupAgentName).newInstance(); + } catch (Exception e) { + throw new RuntimeException("Unable to instantiate backup agent " + + data.appInfo.backupAgentName + ": " + e.toString(), e); + } + + // set up the agent's context + try { + if (DEBUG_BACKUP) Log.v(TAG, "Initializing BackupAgent " + + data.appInfo.backupAgentName); + + ApplicationContext context = new ApplicationContext(); + context.init(packageInfo, null, this); + context.setOuterContext(agent); + agent.attach(context); + agent.onCreate(); + + // tell the OS that we're live now + IBinder binder = agent.onBind(); + try { + ActivityManagerNative.getDefault().backupAgentCreated(packageName, binder); + } catch (RemoteException e) { + // nothing to do. + } + mBackupAgents.put(packageName, agent); + } catch (Exception e) { + throw new RuntimeException("Unable to create BackupAgent " + + data.appInfo.backupAgentName + ": " + e.toString(), e); + } + } + + // Tear down a BackupAgent + private final void handleDestroyBackupAgent(CreateBackupAgentData data) { + if (DEBUG_BACKUP) Log.v(TAG, "handleDestroyBackupAgent: " + data); + + PackageInfo packageInfo = getPackageInfoNoCheck(data.appInfo); + String packageName = packageInfo.mPackageName; + BackupAgent agent = mBackupAgents.get(packageName); + if (agent != null) { + try { + agent.onDestroy(); + } catch (Exception e) { + Log.w(TAG, "Exception thrown in onDestroy by backup agent of " + data.appInfo); + e.printStackTrace(); + } + mBackupAgents.remove(packageName); + } else { + Log.w(TAG, "Attempt to destroy unknown backup agent " + data); + } + } + private final void handleCreateService(CreateServiceData data) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. @@ -2520,7 +2622,7 @@ public final class ActivityThread { ApplicationContext context = new ApplicationContext(); context.init(packageInfo, null, this); - Application app = packageInfo.makeApplication(); + Application app = packageInfo.makeApplication(false); context.setOuterContext(service); service.attach(context, this, data.info.name, data.token, app, ActivityManagerNative.getDefault()); @@ -3134,7 +3236,7 @@ public final class ActivityThread { r.activity.getComponentName().getClassName()); if (!r.activity.mCalled) { throw new SuperNotCalledException( - "Activity " + r.intent.getComponent().toShortString() + "Activity " + safeToComponentShortString(r.intent) + " did not call through to super.onPause()"); } } catch (SuperNotCalledException e) { @@ -3143,7 +3245,7 @@ public final class ActivityThread { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( "Unable to pause activity " - + r.intent.getComponent().toShortString() + + safeToComponentShortString(r.intent) + ": " + e.toString(), e); } } @@ -3158,7 +3260,7 @@ public final class ActivityThread { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( "Unable to stop activity " - + r.intent.getComponent().toShortString() + + safeToComponentShortString(r.intent) + ": " + e.toString(), e); } } @@ -3183,7 +3285,7 @@ public final class ActivityThread { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( "Unable to retain child activities " - + r.intent.getComponent().toShortString() + + safeToComponentShortString(r.intent) + ": " + e.toString(), e); } } @@ -3194,7 +3296,7 @@ public final class ActivityThread { r.activity.onDestroy(); if (!r.activity.mCalled) { throw new SuperNotCalledException( - "Activity " + r.intent.getComponent().toShortString() + + "Activity " + safeToComponentShortString(r.intent) + " did not call through to super.onDestroy()"); } if (r.window != null) { @@ -3205,8 +3307,7 @@ public final class ActivityThread { } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( - "Unable to destroy activity " - + r.intent.getComponent().toShortString() + "Unable to destroy activity " + safeToComponentShortString(r.intent) + ": " + e.toString(), e); } } @@ -3216,6 +3317,11 @@ public final class ActivityThread { return r; } + private static String safeToComponentShortString(Intent intent) { + ComponentName component = intent.getComponent(); + return component == null ? "[Unknown]" : component.toShortString(); + } + private final void handleDestroyActivity(IBinder token, boolean finishing, int configChanges, boolean getNonConfigInstance) { ActivityRecord r = performDestroyActivity(token, finishing, @@ -3475,8 +3581,6 @@ public final class ActivityThread { } mConfiguration.updateFrom(config); DisplayMetrics dm = getDisplayMetricsLocked(true); - DisplayMetrics appDm = new DisplayMetrics(); - appDm.setTo(dm); // set it for java, this also affects newly created Resources if (config.locale != null) { @@ -3496,11 +3600,7 @@ public final class ActivityThread { WeakReference<Resources> v = it.next(); Resources r = v.get(); if (r != null) { - // keep the original density based on application cale. - appDm.updateDensity(r.getDisplayMetrics().density); - r.updateConfiguration(config, appDm); - // reset - appDm.setTo(dm); + r.updateConfiguration(config, dm); //Log.i(TAG, "Updated app resources " + v.getKey() // + " " + r + ": " + r.getConfiguration()); } else { @@ -3528,15 +3628,20 @@ public final class ActivityThread { performConfigurationChanged(r.activity, mConfiguration); } - final void handleProfilerControl(boolean start, String path) { + final void handleProfilerControl(boolean start, ProfilerControlData pcd) { if (start) { - File file = new File(path); - file.getParentFile().mkdirs(); try { - Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024); + Debug.startMethodTracing(pcd.path, pcd.fd.getFileDescriptor(), + 8 * 1024 * 1024, 0); } catch (RuntimeException e) { - Log.w(TAG, "Profiling failed on path " + path + Log.w(TAG, "Profiling failed on path " + pcd.path + " -- can the process access this path?"); + } finally { + try { + pcd.fd.close(); + } catch (IOException e) { + Log.w(TAG, "Failure closing profile fd", e); + } } } else { Debug.stopMethodTracing(); @@ -3592,6 +3697,13 @@ public final class ActivityThread { */ Locale.setDefault(data.config.locale); + /* + * Update the system configuration since its preloaded and might not + * reflect configuration changes. The configuration object passed + * in AppBindData can be safely assumed to be up to date + */ + Resources.getSystem().updateConfiguration(mConfiguration, null); + data.info = getPackageInfoNoCheck(data.appInfo); if (data.debugMode != IApplicationThread.DEBUG_OFF) { @@ -3682,7 +3794,9 @@ public final class ActivityThread { mInstrumentation = new Instrumentation(); } - Application app = data.info.makeApplication(); + // If the app is being launched for full backup or restore, bring it up in + // a restricted environment with the base application class. + Application app = data.info.makeApplication(data.restrictedBackupMode); mInitialApplication = app; List<ProviderInfo> providers = data.providers; @@ -3867,7 +3981,10 @@ public final class ActivityThread { ProviderRecord pr = mProviderMap.get(name); if (pr.mProvider.asBinder() == provider.asBinder()) { Log.i(TAG, "Removing dead content provider: " + name); - mProviderMap.remove(name); + ProviderRecord removed = mProviderMap.remove(name); + if (removed != null) { + removed.mProvider.asBinder().unlinkToDeath(removed, 0); + } } } } @@ -3876,7 +3993,10 @@ public final class ActivityThread { ProviderRecord pr = mProviderMap.get(name); if (pr.mProvider.asBinder() == provider.asBinder()) { Log.i(TAG, "Removing dead content provider: " + name); - mProviderMap.remove(name); + ProviderRecord removed = mProviderMap.remove(name); + if (removed != null) { + removed.mProvider.asBinder().unlinkToDeath(removed, 0); + } } } diff --git a/core/java/android/app/ApplicationContext.java b/core/java/android/app/ApplicationContext.java index bb17dc3..38ea686 100644 --- a/core/java/android/app/ApplicationContext.java +++ b/core/java/android/app/ApplicationContext.java @@ -16,8 +16,11 @@ package android.app; -import com.google.android.collect.Maps; +import com.android.internal.policy.PolicyManager; import com.android.internal.util.XmlUtils; +import com.google.android.collect.Maps; + +import org.xmlpull.v1.XmlPullParserException; import android.bluetooth.BluetoothDevice; import android.bluetooth.IBluetoothDevice; @@ -29,6 +32,8 @@ import android.content.ContextWrapper; import android.content.IContentProvider; import android.content.Intent; import android.content.IntentFilter; +import android.content.IIntentReceiver; +import android.content.IntentSender; import android.content.ReceiverCallNotAllowedException; import android.content.ServiceConnection; import android.content.SharedPreferences; @@ -37,9 +42,9 @@ import android.content.pm.ApplicationInfo; import android.content.pm.ComponentInfo; import android.content.pm.IPackageDataObserver; import android.content.pm.IPackageDeleteObserver; -import android.content.pm.IPackageStatsObserver; import android.content.pm.IPackageInstallObserver; import android.content.pm.IPackageManager; +import android.content.pm.IPackageStatsObserver; import android.content.pm.InstrumentationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -68,29 +73,30 @@ import android.net.wifi.IWifiManager; import android.net.wifi.WifiManager; import android.os.Binder; import android.os.Bundle; -import android.os.Looper; -import android.os.RemoteException; import android.os.FileUtils; import android.os.Handler; import android.os.IBinder; import android.os.IPowerManager; +import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.Process; +import android.os.RemoteException; import android.os.ServiceManager; import android.os.Vibrator; import android.os.FileUtils.FileStatus; import android.telephony.TelephonyManager; import android.text.ClipboardManager; import android.util.AndroidRuntimeException; +import android.util.DisplayMetrics; import android.util.Log; import android.view.ContextThemeWrapper; +import android.view.Display; import android.view.LayoutInflater; import android.view.WindowManagerImpl; +import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.InputMethodManager; -import com.android.internal.policy.PolicyManager; - import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -100,16 +106,14 @@ import java.io.InputStream; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.WeakHashMap; import java.util.Set; -import java.util.HashSet; +import java.util.WeakHashMap; import java.util.Map.Entry; -import org.xmlpull.v1.XmlPullParserException; - class ReceiverRestrictedContext extends ContextWrapper { ReceiverRestrictedContext(Context base) { super(base); @@ -147,6 +151,7 @@ class ReceiverRestrictedContext extends ContextWrapper { */ class ApplicationContext extends Context { private final static String TAG = "ApplicationContext"; + private final static boolean DEBUG = false; private final static boolean DEBUG_ICONS = false; private static final Object sSync = new Object(); @@ -172,6 +177,7 @@ class ApplicationContext extends Context { private Resources.Theme mTheme = null; private PackageManager mPackageManager; private NotificationManager mNotificationManager = null; + private AccessibilityManager mAccessibilityManager = null; private ActivityManager mActivityManager = null; private Context mReceiverRestrictedContext = null; private SearchManager mSearchManager = null; @@ -181,6 +187,7 @@ class ApplicationContext extends Context { private StatusBarManager mStatusBarManager = null; private TelephonyManager mTelephonyManager = null; private ClipboardManager mClipboardManager = null; + private boolean mRestricted; private final Object mSync = new Object(); @@ -280,6 +287,14 @@ class ApplicationContext extends Context { } @Override + public ApplicationInfo getApplicationInfo() { + if (mPackageInfo != null) { + return mPackageInfo.getApplicationInfo(); + } + throw new RuntimeException("Not supported in system context"); + } + + @Override public String getPackageResourcePath() { if (mPackageInfo != null) { return mPackageInfo.getResDir(); @@ -299,10 +314,14 @@ class ApplicationContext extends Context { return new File(prefsFile.getPath() + ".bak"); } + public File getSharedPrefsFile(String name) { + return makeFilename(getPreferencesDir(), name + ".xml"); + } + @Override public SharedPreferences getSharedPreferences(String name, int mode) { SharedPreferencesImpl sp; - File f = makeFilename(getPreferencesDir(), name + ".xml"); + File f = getSharedPrefsFile(name); synchronized (sSharedPrefs) { sp = sSharedPrefs.get(f); if (sp != null && !sp.hasFileChanged()) { @@ -550,19 +569,6 @@ class ApplicationContext extends Context { } } - /** - * @hide - */ - @Override - public float getApplicationScale() { - if (mPackageInfo != null) { - return mPackageInfo.getApplicationScale(); - } else { - // same as system density - return 1.0f; - } - } - @Override public void setWallpaper(Bitmap bitmap) throws IOException { try { @@ -904,6 +910,8 @@ class ApplicationContext extends Context { return getNotificationManager(); } else if (KEYGUARD_SERVICE.equals(name)) { return new KeyguardManager(); + } else if (ACCESSIBILITY_SERVICE.equals(name)) { + return AccessibilityManager.getInstance(this); } else if (LOCATION_SERVICE.equals(name)) { return getLocationManager(); } else if (SEARCH_SERVICE.equals(name)) { @@ -1033,11 +1041,6 @@ class ApplicationContext extends Context { } private SearchManager getSearchManager() { - // This is only useable in Activity Contexts - if (getActivityToken() == null) { - throw new AndroidRuntimeException( - "Acquiring SearchManager objects only valid in Activity Contexts."); - } synchronized (mSync) { if (mSearchManager == null) { mSearchManager = new SearchManager(getOuterContext(), mMainThread.getHandler()); @@ -1238,7 +1241,7 @@ class ApplicationContext extends Context { @Override public int checkUriPermission(Uri uri, String readPermission, String writePermission, int pid, int uid, int modeFlags) { - if (false) { + if (DEBUG) { Log.i("foo", "checkUriPermission: uri=" + uri + "readPermission=" + readPermission + " writePermission=" + writePermission + " pid=" + pid + " uid=" + uid + " mode" + modeFlags); @@ -1337,8 +1340,22 @@ class ApplicationContext extends Context { mMainThread.getPackageInfo(packageName, flags); if (pi != null) { ApplicationContext c = new ApplicationContext(); + c.mRestricted = (flags & CONTEXT_RESTRICTED) == CONTEXT_RESTRICTED; c.init(pi, null, mMainThread); if (c.mResources != null) { + Resources newRes = c.mResources; + if (mResources.getCompatibilityInfo().applicationScale != + newRes.getCompatibilityInfo().applicationScale) { + DisplayMetrics dm = mMainThread.getDisplayMetricsLocked(false); + c.mResources = new Resources(newRes.getAssets(), dm, + newRes.getConfiguration(), + mResources.getCompatibilityInfo().copy()); + if (DEBUG) { + Log.d(TAG, "loaded context has different scaling. Using container's" + + " compatiblity info:" + mResources.getDisplayMetrics()); + } + + } return c; } } @@ -1348,6 +1365,11 @@ class ApplicationContext extends Context { "Application package " + packageName + " not found"); } + @Override + public boolean isRestricted() { + return mRestricted; + } + private File getDataDirFile() { if (mPackageInfo != null) { return mPackageInfo.getDataDirFile(); @@ -1453,7 +1475,7 @@ class ApplicationContext extends Context { if ((mode&MODE_WORLD_WRITEABLE) != 0) { perms |= FileUtils.S_IWOTH; } - if (false) { + if (DEBUG) { Log.i(TAG, "File " + name + ": mode=0x" + Integer.toHexString(mode) + ", perms=0x" + Integer.toHexString(perms)); } @@ -1516,43 +1538,33 @@ class ApplicationContext extends Context { throw new NameNotFoundException(packageName); } - public Intent getLaunchIntentForPackage(String packageName) - throws NameNotFoundException { + @Override + public Intent getLaunchIntentForPackage(String packageName) { // First see if the package has an INFO activity; the existence of // such an activity is implied to be the desired front-door for the // overall package (such as if it has multiple launcher entries). - Intent intent = getLaunchIntentForPackageCategory(this, packageName, - Intent.CATEGORY_INFO); - if (intent != null) { - return intent; - } - + Intent intentToResolve = new Intent(Intent.ACTION_MAIN); + intentToResolve.addCategory(Intent.CATEGORY_INFO); + intentToResolve.setPackage(packageName); + ResolveInfo resolveInfo = resolveActivity(intentToResolve, 0); + // Otherwise, try to find a main launcher activity. - return getLaunchIntentForPackageCategory(this, packageName, - Intent.CATEGORY_LAUNCHER); - } - - // XXX This should be implemented as a call to the package manager, - // to reduce the work needed. - static Intent getLaunchIntentForPackageCategory(PackageManager pm, - String packageName, String category) { + if (resolveInfo == null) { + // reuse the intent instance + intentToResolve.removeCategory(Intent.CATEGORY_INFO); + intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER); + intentToResolve.setPackage(packageName); + resolveInfo = resolveActivity(intentToResolve, 0); + } + if (resolveInfo == null) { + return null; + } Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(packageName, resolveInfo.activityInfo.name); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Intent intentToResolve = new Intent(Intent.ACTION_MAIN, null); - intentToResolve.addCategory(category); - final List<ResolveInfo> apps = - pm.queryIntentActivities(intentToResolve, 0); - // I wish there were a way to directly get the "main" activity of a - // package but ... - for (ResolveInfo app : apps) { - if (app.activityInfo.packageName.equals(packageName)) { - intent.setClassName(packageName, app.activityInfo.name); - return intent; - } - } - return null; + return intent; } - + @Override public int[] getPackageGids(String packageName) throws NameNotFoundException { @@ -2024,8 +2036,7 @@ class ApplicationContext extends Context { ActivityThread.PackageInfo pi = mContext.mMainThread.getPackageInfoNoCheck(app); Resources r = mContext.mMainThread.getTopLevelResources( app.uid == Process.myUid() ? app.sourceDir - : app.publicSourceDir, - pi.getApplicationScale()); + : app.publicSourceDir, pi); if (r != null) { return r; } @@ -2363,11 +2374,11 @@ class ApplicationContext extends Context { // Should never happen! } } - + @Override - public void freeStorage(long idealStorageSize, PendingIntent opFinishedIntent) { + public void freeStorage(long freeStorageSize, IntentSender pi) { try { - mPM.freeStorage(idealStorageSize, opFinishedIntent); + mPM.freeStorage(freeStorageSize, pi); } catch (RemoteException e) { // Should never happen! } @@ -2421,6 +2432,16 @@ class ApplicationContext extends Context { } @Override + public void replacePreferredActivity(IntentFilter filter, + int match, ComponentName[] set, ComponentName activity) { + try { + mPM.replacePreferredActivity(filter, match, set, activity); + } catch (RemoteException e) { + // Should never happen! + } + } + + @Override public void clearPackagePreferredActivities(String packageName) { try { mPM.clearPackagePreferredActivities(packageName); diff --git a/core/java/android/app/ApplicationErrorReport.java b/core/java/android/app/ApplicationErrorReport.java new file mode 100644 index 0000000..6b17236 --- /dev/null +++ b/core/java/android/app/ApplicationErrorReport.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Printer; + +/** + * Describes an application error. + * + * A report has a type, which is one of + * <ul> + * <li> {@link #TYPE_CRASH} application crash. Information about the crash + * is stored in {@link #crashInfo}. + * <li> {@link #TYPE_ANR} application not responding. Information about the + * ANR is stored in {@link #anrInfo}. + * <li> {@link #TYPE_NONE} uninitialized instance of {@link ApplicationErrorReport}. + * </ul> + * + * @hide + */ + +public class ApplicationErrorReport implements Parcelable { + /** + * Uninitialized error report. + */ + public static final int TYPE_NONE = 0; + + /** + * An error report about an application crash. + */ + public static final int TYPE_CRASH = 1; + + /** + * An error report about an application that's not responding. + */ + public static final int TYPE_ANR = 2; + + /** + * Type of this report. Can be one of {@link #TYPE_NONE}, + * {@link #TYPE_CRASH} or {@link #TYPE_ANR}. + */ + public int type; + + /** + * Package name of the application. + */ + public String packageName; + + /** + * Package name of the application which installed the application this + * report pertains to. + * This identifies which Market the application came from. + */ + public String installerPackageName; + + /** + * Process name of the application. + */ + public String processName; + + /** + * Time at which the error occurred. + */ + public long time; + + /** + * If this report is of type {@link #TYPE_CRASH}, contains an instance + * of CrashInfo describing the crash; otherwise null. + */ + public CrashInfo crashInfo; + + /** + * If this report is of type {@link #TYPE_ANR}, contains an instance + * of AnrInfo describing the ANR; otherwise null. + */ + public AnrInfo anrInfo; + + /** + * Create an uninitialized instance of {@link ApplicationErrorReport}. + */ + public ApplicationErrorReport() { + } + + /** + * Create an instance of {@link ApplicationErrorReport} initialized from + * a parcel. + */ + ApplicationErrorReport(Parcel in) { + readFromParcel(in); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(type); + dest.writeString(packageName); + dest.writeString(installerPackageName); + dest.writeString(processName); + dest.writeLong(time); + + switch (type) { + case TYPE_CRASH: + crashInfo.writeToParcel(dest, flags); + break; + case TYPE_ANR: + anrInfo.writeToParcel(dest, flags); + break; + } + } + + public void readFromParcel(Parcel in) { + type = in.readInt(); + packageName = in.readString(); + installerPackageName = in.readString(); + processName = in.readString(); + time = in.readLong(); + + switch (type) { + case TYPE_CRASH: + crashInfo = new CrashInfo(in); + anrInfo = null; + break; + case TYPE_ANR: + anrInfo = new AnrInfo(in); + crashInfo = null; + break; + } + } + + /** + * Describes an application crash. + */ + public static class CrashInfo { + /** + * Class name of the exception that caused the crash. + */ + public String exceptionClassName; + + /** + * Message stored in the exception. + */ + public String exceptionMessage; + + /** + * File which the exception was thrown from. + */ + public String throwFileName; + + /** + * Class which the exception was thrown from. + */ + public String throwClassName; + + /** + * Method which the exception was thrown from. + */ + public String throwMethodName; + + /** + * Stack trace. + */ + public String stackTrace; + + /** + * Create an uninitialized instance of CrashInfo. + */ + public CrashInfo() { + } + + /** + * Create an instance of CrashInfo initialized from a Parcel. + */ + public CrashInfo(Parcel in) { + exceptionClassName = in.readString(); + exceptionMessage = in.readString(); + throwFileName = in.readString(); + throwClassName = in.readString(); + throwMethodName = in.readString(); + stackTrace = in.readString(); + } + + /** + * Save a CrashInfo instance to a parcel. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(exceptionClassName); + dest.writeString(exceptionMessage); + dest.writeString(throwFileName); + dest.writeString(throwClassName); + dest.writeString(throwMethodName); + dest.writeString(stackTrace); + } + + /** + * Dump a CrashInfo instance to a Printer. + */ + public void dump(Printer pw, String prefix) { + pw.println(prefix + "exceptionClassName: " + exceptionClassName); + pw.println(prefix + "exceptionMessage: " + exceptionMessage); + pw.println(prefix + "throwFileName: " + throwFileName); + pw.println(prefix + "throwClassName: " + throwClassName); + pw.println(prefix + "throwMethodName: " + throwMethodName); + pw.println(prefix + "stackTrace: " + stackTrace); + } + } + + /** + * Describes an application not responding error. + */ + public static class AnrInfo { + /** + * Activity name. + */ + public String activity; + + /** + * Description of the operation that timed out. + */ + public String cause; + + /** + * Additional info, including CPU stats. + */ + public String info; + + /** + * Create an uninitialized instance of AnrInfo. + */ + public AnrInfo() { + } + + /** + * Create an instance of AnrInfo initialized from a Parcel. + */ + public AnrInfo(Parcel in) { + activity = in.readString(); + cause = in.readString(); + info = in.readString(); + } + + /** + * Save an AnrInfo instance to a parcel. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(activity); + dest.writeString(cause); + dest.writeString(info); + } + + /** + * Dump an AnrInfo instance to a Printer. + */ + public void dump(Printer pw, String prefix) { + pw.println(prefix + "activity: " + activity); + pw.println(prefix + "cause: " + cause); + pw.println(prefix + "info: " + info); + } + } + + public static final Parcelable.Creator<ApplicationErrorReport> CREATOR + = new Parcelable.Creator<ApplicationErrorReport>() { + public ApplicationErrorReport createFromParcel(Parcel source) { + return new ApplicationErrorReport(source); + } + + public ApplicationErrorReport[] newArray(int size) { + return new ApplicationErrorReport[size]; + } + }; + + public int describeContents() { + return 0; + } + + /** + * Dump the report to a Printer. + */ + public void dump(Printer pw, String prefix) { + pw.println(prefix + "type: " + type); + pw.println(prefix + "packageName: " + packageName); + pw.println(prefix + "installerPackageName: " + installerPackageName); + pw.println(prefix + "processName: " + processName); + pw.println(prefix + "time: " + time); + + switch (type) { + case TYPE_CRASH: + crashInfo.dump(pw, prefix); + break; + case TYPE_ANR: + anrInfo.dump(pw, prefix); + break; + } + } +} diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index bcc9302..b052c99 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -18,6 +18,7 @@ package android.app; import android.content.ComponentName; import android.content.Intent; +import android.content.IIntentReceiver; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ProviderInfo; @@ -25,6 +26,7 @@ import android.content.pm.ServiceInfo; import android.content.res.Configuration; import android.os.Binder; import android.os.Bundle; +import android.os.Parcelable; import android.os.RemoteException; import android.os.IBinder; import android.os.Parcel; @@ -230,11 +232,13 @@ public abstract class ApplicationThreadNative extends Binder IBinder binder = data.readStrongBinder(); IInstrumentationWatcher testWatcher = IInstrumentationWatcher.Stub.asInterface(binder); int testMode = data.readInt(); + boolean restrictedBackupMode = (data.readInt() != 0); Configuration config = Configuration.CREATOR.createFromParcel(data); HashMap<String, IBinder> services = data.readHashMap(null); bindApplication(packageName, info, providers, testName, profileName, - testArgs, testWatcher, testMode, config, services); + testArgs, testWatcher, testMode, restrictedBackupMode, + config, services); return true; } @@ -328,7 +332,34 @@ public abstract class ApplicationThreadNative extends Binder data.enforceInterface(IApplicationThread.descriptor); boolean start = data.readInt() != 0; String path = data.readString(); - profilerControl(start, path); + ParcelFileDescriptor fd = data.readInt() != 0 + ? data.readFileDescriptor() : null; + profilerControl(start, path, fd); + return true; + } + + case SET_SCHEDULING_GROUP_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + int group = data.readInt(); + setSchedulingGroup(group); + return true; + } + + case SCHEDULE_CREATE_BACKUP_AGENT_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + ApplicationInfo appInfo = ApplicationInfo.CREATOR.createFromParcel(data); + int backupMode = data.readInt(); + scheduleCreateBackupAgent(appInfo, backupMode); + return true; + } + + case SCHEDULE_DESTROY_BACKUP_AGENT_TRANSACTION: + { + data.enforceInterface(IApplicationThread.descriptor); + ApplicationInfo appInfo = ApplicationInfo.CREATOR.createFromParcel(data); + scheduleDestroyBackupAgent(appInfo); return true; } } @@ -484,6 +515,24 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } + public final void scheduleCreateBackupAgent(ApplicationInfo app, int backupMode) + throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + app.writeToParcel(data, 0); + data.writeInt(backupMode); + mRemote.transact(SCHEDULE_CREATE_BACKUP_AGENT_TRANSACTION, data, null, 0); + data.recycle(); + } + + public final void scheduleDestroyBackupAgent(ApplicationInfo app) throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + app.writeToParcel(data, 0); + mRemote.transact(SCHEDULE_DESTROY_BACKUP_AGENT_TRANSACTION, data, null, 0); + data.recycle(); + } + public final void scheduleCreateService(IBinder token, ServiceInfo info) throws RemoteException { Parcel data = Parcel.obtain(); @@ -543,7 +592,8 @@ class ApplicationThreadProxy implements IApplicationThread { public final void bindApplication(String packageName, ApplicationInfo info, List<ProviderInfo> providers, ComponentName testName, String profileName, Bundle testArgs, IInstrumentationWatcher testWatcher, int debugMode, - Configuration config, Map<String, IBinder> services) throws RemoteException { + boolean restrictedBackupMode, Configuration config, + Map<String, IBinder> services) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeString(packageName); @@ -559,6 +609,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeBundle(testArgs); data.writeStrongInterface(testWatcher); data.writeInt(debugMode); + data.writeInt(restrictedBackupMode ? 1 : 0); config.writeToParcel(data, 0); data.writeMap(services); mRemote.transact(BIND_APPLICATION_TRANSACTION, data, null, @@ -663,14 +714,30 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } - public void profilerControl(boolean start, String path) throws RemoteException { + public void profilerControl(boolean start, String path, + ParcelFileDescriptor fd) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeInt(start ? 1 : 0); data.writeString(path); + if (fd != null) { + data.writeInt(1); + fd.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + data.writeInt(0); + } mRemote.transact(PROFILER_CONTROL_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); } + + public void setSchedulingGroup(int group) throws RemoteException { + Parcel data = Parcel.obtain(); + data.writeInterfaceToken(IApplicationThread.descriptor); + data.writeInt(group); + mRemote.transact(SET_SCHEDULING_GROUP_TRANSACTION, data, null, + IBinder.FLAG_ONEWAY); + data.recycle(); + } } diff --git a/core/java/android/backup/BackupService.java b/core/java/android/app/BackupAgent.java index 50a5921..0ac8a1e 100644 --- a/core/java/android/backup/BackupService.java +++ b/core/java/android/app/BackupAgent.java @@ -14,47 +14,38 @@ * limitations under the License. */ -package android.backup; +package android.app; -import android.annotation.SdkConstant; -import android.annotation.SdkConstant.SdkConstantType; -import android.app.Service; -import android.backup.IBackupService; -import android.content.Intent; +import android.app.IBackupAgent; +import android.backup.BackupDataInput; +import android.backup.BackupDataOutput; +import android.content.Context; +import android.content.ContextWrapper; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; +import java.io.IOException; + /** * This is the central interface between an application and Android's * settings backup mechanism. * - * In order to use the backup service, your application must implement a - * subclass of BackupService, and declare an intent filter - * in the application manifest specifying that your BackupService subclass - * handles the {@link BackupService#SERVICE_ACTION} intent action. For example: - * - * <pre class="prettyprint"> - * <!-- Use the class "MyBackupService" to perform backups for my app --> - * <service android:name=".MyBackupService"> - * <intent-filter> - * <action android:name="android.backup.BackupService.SERVICE" /> - * </intent-filter> - * </service></pre> - * * @hide pending API solidification */ +public abstract class BackupAgent extends ContextWrapper { + private static final String TAG = "BackupAgent"; -public abstract class BackupService extends Service { - /** - * Service Action: Participate in the backup infrastructure. Applications - * that wish to use the Android backup mechanism must provide an exported - * subclass of BackupService and give it an {@link android.content.IntentFilter - * IntentFilter} that accepts this action. - */ - @SdkConstant(SdkConstantType.SERVICE_ACTION) - public static final String SERVICE_ACTION = "android.backup.BackupService.SERVICE"; + public BackupAgent() { + super(null); + } + + public void onCreate() { + } + + public void onDestroy() { + } /** * The application is being asked to write any data changed since the @@ -76,7 +67,7 @@ public abstract class BackupService extends Service { * here after writing the requested data to dataFd. */ public abstract void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, - ParcelFileDescriptor newState); + ParcelFileDescriptor newState) throws IOException; /** * The application is being restored from backup, and should replace any @@ -87,11 +78,17 @@ public abstract class BackupService extends Service { * * @param data An open, read-only ParcelFileDescriptor pointing to a full snapshot * of the application's data. + * @param appVersionCode The android:versionCode value of the application that backed + * up this particular data set. This makes it easier for an application's + * agent to distinguish among several possible older data versions when + * asked to perform the restore operation. * @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 dataFd. */ - public abstract void onRestore(ParcelFileDescriptor /* TODO: BackupDataInput */ data, ParcelFileDescriptor newState); + public abstract void onRestore(BackupDataInput data, int appVersionCode, + ParcelFileDescriptor newState) + throws IOException; // ----- Core implementation ----- @@ -100,38 +97,52 @@ public abstract class BackupService extends Service { * Returns the private interface called by the backup system. Applications will * not typically override this. */ - public IBinder onBind(Intent intent) { - if (intent.getAction().equals(SERVICE_ACTION)) { - return mBinder; - } - return null; + public IBinder onBind() { + return mBinder; } private final IBinder mBinder = new BackupServiceBinder().asBinder(); + /** @hide */ + public void attach(Context context) { + attachBaseContext(context); + } + // ----- IBackupService binder interface ----- - private class BackupServiceBinder extends IBackupService.Stub { + private class BackupServiceBinder extends IBackupAgent.Stub { + private static final String TAG = "BackupServiceBinder"; + public void doBackup(ParcelFileDescriptor oldState, ParcelFileDescriptor data, ParcelFileDescriptor newState) throws RemoteException { // !!! TODO - real implementation; for now just invoke the callbacks directly - Log.v("BackupServiceBinder", "doBackup() invoked"); - BackupDataOutput output = new BackupDataOutput(BackupService.this, - data.getFileDescriptor()); + Log.v(TAG, "doBackup() invoked"); + BackupDataOutput output = new BackupDataOutput(data.getFileDescriptor()); try { - BackupService.this.onBackup(oldState, output, newState); + BackupAgent.this.onBackup(oldState, output, newState); + } catch (IOException ex) { + Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); + throw new RuntimeException(ex); } catch (RuntimeException ex) { - Log.d("BackupService", "onBackup (" - + BackupService.this.getClass().getName() + ") threw", ex); + Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); throw ex; } } - public void doRestore(ParcelFileDescriptor data, + public void doRestore(ParcelFileDescriptor data, int appVersionCode, ParcelFileDescriptor newState) throws RemoteException { // !!! TODO - real implementation; for now just invoke the callbacks directly - Log.v("BackupServiceBinder", "doRestore() invoked"); - BackupService.this.onRestore(data, newState); + Log.v(TAG, "doRestore() invoked"); + BackupDataInput input = new BackupDataInput(data.getFileDescriptor()); + try { + BackupAgent.this.onRestore(input, appVersionCode, newState); + } catch (IOException ex) { + Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex); + throw new RuntimeException(ex); + } catch (RuntimeException ex) { + Log.d(TAG, "onRestore (" + BackupAgent.this.getClass().getName() + ") threw", ex); + throw ex; + } } } } diff --git a/core/java/android/app/DatePickerDialog.java b/core/java/android/app/DatePickerDialog.java index 863cbcc..78bbb4f 100644 --- a/core/java/android/app/DatePickerDialog.java +++ b/core/java/android/app/DatePickerDialog.java @@ -46,7 +46,6 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, private final DatePicker mDatePicker; private final OnDateSetListener mCallBack; private final Calendar mCalendar; - private final java.text.DateFormat mDateFormat; private final java.text.DateFormat mTitleDateFormat; private final String[] mWeekDays; @@ -108,7 +107,6 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, DateFormatSymbols symbols = new DateFormatSymbols(); mWeekDays = symbols.getShortWeekdays(); - mDateFormat = DateFormat.getMediumDateFormat(context); mTitleDateFormat = java.text.DateFormat. getDateInstance(java.text.DateFormat.FULL); mCalendar = Calendar.getInstance(); diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index b09a57f..222fe75 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -16,32 +16,34 @@ package android.app; +import com.android.internal.policy.PolicyManager; + import android.content.Context; import android.content.DialogInterface; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.os.Bundle; import android.util.Config; import android.util.Log; import android.view.ContextMenu; import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.LayoutInflater; import android.view.Window; import android.view.WindowManager; import android.view.ContextMenu.ContextMenuInfo; import android.view.View.OnCreateContextMenuListener; - -import com.android.internal.policy.PolicyManager; +import android.view.ViewGroup.LayoutParams; +import android.view.accessibility.AccessibilityEvent; import java.lang.ref.WeakReference; @@ -81,6 +83,7 @@ public class Dialog implements DialogInterface, Window.Callback, * {@hide} */ protected boolean mCancelable = true; + private Message mCancelMessage; private Message mDismissMessage; @@ -209,7 +212,9 @@ public class Dialog implements DialogInterface, Window.Callback, if (mShowing) { if (Config.LOGV) Log.v(LOG_TAG, "[Dialog] start: already showing, ignore"); - if (mDecor != null) mDecor.setVisibility(View.VISIBLE); + if (mDecor != null) { + mDecor.setVisibility(View.VISIBLE); + } return; } @@ -236,7 +241,9 @@ public class Dialog implements DialogInterface, Window.Callback, * Hide the dialog, but do not dismiss it. */ public void hide() { - if (mDecor != null) mDecor.setVisibility(View.GONE); + if (mDecor != null) { + mDecor.setVisibility(View.GONE); + } } /** @@ -266,6 +273,7 @@ public class Dialog implements DialogInterface, Window.Callback, } mWindowManager.removeView(mDecor); + mDecor = null; mWindow.closeAllPanels(); onStop(); @@ -280,7 +288,7 @@ public class Dialog implements DialogInterface, Window.Callback, Message.obtain(mDismissMessage).sendToTarget(); } } - + // internal method to make sure mcreated is set properly without requiring // users to call through to super in onCreate void dispatchOnCreate(Bundle savedInstanceState) { @@ -608,6 +616,18 @@ public class Dialog implements DialogInterface, Window.Callback, return onTrackballEvent(ev); } + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(getClass().getName()); + event.setPackageName(mContext.getPackageName()); + + LayoutParams params = getWindow().getAttributes(); + boolean isFullScreen = (params.width == LayoutParams.FILL_PARENT) && + (params.height == LayoutParams.FILL_PARENT); + event.setFullScreen(isFullScreen); + + return false; + } + /** * @see Activity#onCreatePanelView(int) */ diff --git a/core/java/android/app/FullBackupAgent.java b/core/java/android/app/FullBackupAgent.java new file mode 100644 index 0000000..d89db96 --- /dev/null +++ b/core/java/android/app/FullBackupAgent.java @@ -0,0 +1,58 @@ +package android.app; + +import android.backup.BackupDataInput; +import android.backup.BackupDataOutput; +import android.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, (String[])allFiles.toArray()); + 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 56b29c1..3ec7938 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -21,6 +21,9 @@ import android.content.ContentProviderNative; import android.content.IContentProvider; import android.content.Intent; import android.content.IntentFilter; +import android.content.IIntentSender; +import android.content.IIntentReceiver; +import android.content.pm.ApplicationInfo; import android.content.pm.ConfigurationInfo; import android.content.pm.IPackageDataObserver; import android.content.pm.ProviderInfo; @@ -44,9 +47,30 @@ import java.util.List; * {@hide} */ public interface IActivityManager extends IInterface { + /** + * Returned by startActivity() if the start request was canceled because + * app switches are temporarily canceled to ensure the user's last request + * (such as pressing home) is performed. + */ + public static final int START_SWITCHES_CANCELED = 4; + /** + * Returned by startActivity() if an activity wasn't really started, but + * the given Intent was given to the existing top activity. + */ public static final int START_DELIVERED_TO_TOP = 3; + /** + * Returned by startActivity() if an activity wasn't really started, but + * a task was simply brought to the foreground. + */ public static final int START_TASK_TO_FRONT = 2; + /** + * Returned by startActivity() if the caller asked that the Intent not + * be executed if it is the recipient, and that is indeed the case. + */ public static final int START_RETURN_INTENT_TO_CALLER = 1; + /** + * Activity was started successfully as normal. + */ public static final int START_SUCCESS = 0; public static final int START_INTENT_NOT_RESOLVED = -1; public static final int START_CLASS_NOT_FOUND = -2; @@ -128,6 +152,11 @@ public interface IActivityManager extends IInterface { public void serviceDoneExecuting(IBinder token) throws RemoteException; public IBinder peekService(Intent service, String resolvedType) throws RemoteException; + public boolean bindBackupAgent(ApplicationInfo appInfo, int backupRestoreMode) + throws RemoteException; + public void backupAgentCreated(String packageName, IBinder agent) throws RemoteException; + public void unbindBackupAgent(ApplicationInfo appInfo) throws RemoteException; + public boolean startInstrumentation(ComponentName className, String profileFile, int flags, Bundle arguments, IInstrumentationWatcher watcher) throws RemoteException; @@ -221,10 +250,13 @@ public interface IActivityManager extends IInterface { // Turn on/off profiling in a particular process. public boolean profileControl(String process, boolean start, - String path) throws RemoteException; + String path, ParcelFileDescriptor fd) throws RemoteException; public boolean shutdown(int timeout) throws RemoteException; + public void stopAppSwitches() throws RemoteException; + public void resumeAppSwitches() throws RemoteException; + /* * Private non-Binder interfaces */ @@ -371,4 +403,9 @@ public interface IActivityManager extends IInterface { int PEEK_SERVICE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+84; int PROFILE_CONTROL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+85; int SHUTDOWN_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+86; + int STOP_APP_SWITCHES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+87; + int RESUME_APP_SWITCHES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+88; + int START_BACKUP_AGENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+89; + int BACKUP_AGENT_CREATED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+90; + int UNBIND_BACKUP_AGENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+91; } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index 9f3534b..c0bc2a0 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -18,12 +18,14 @@ package android.app; import android.content.ComponentName; import android.content.Intent; +import android.content.IIntentReceiver; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.content.res.Configuration; import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.IBinder; import android.os.IInterface; @@ -59,6 +61,11 @@ public interface IApplicationThread extends IInterface { int configChanges) throws RemoteException; void scheduleReceiver(Intent intent, ActivityInfo info, int resultCode, String data, Bundle extras, boolean sync) throws RemoteException; + static final int BACKUP_MODE_INCREMENTAL = 0; + static final int BACKUP_MODE_FULL = 1; + static final int BACKUP_MODE_RESTORE = 2; + void scheduleCreateBackupAgent(ApplicationInfo app, int backupMode) throws RemoteException; + void scheduleDestroyBackupAgent(ApplicationInfo app) throws RemoteException; void scheduleCreateService(IBinder token, ServiceInfo info) throws RemoteException; void scheduleBindService(IBinder token, Intent intent, boolean rebind) throws RemoteException; @@ -71,8 +78,8 @@ public interface IApplicationThread extends IInterface { static final int DEBUG_WAIT = 2; void bindApplication(String packageName, ApplicationInfo info, List<ProviderInfo> providers, ComponentName testName, String profileName, Bundle testArguments, - IInstrumentationWatcher testWatcher, int debugMode, Configuration config, Map<String, - IBinder> services) throws RemoteException; + IInstrumentationWatcher testWatcher, int debugMode, boolean restrictedBackupMode, + Configuration config, Map<String, IBinder> services) throws RemoteException; void scheduleExit() throws RemoteException; void requestThumbnail(IBinder token) throws RemoteException; void scheduleConfigurationChanged(Configuration config) throws RemoteException; @@ -86,8 +93,10 @@ public interface IApplicationThread extends IInterface { void scheduleLowMemory() throws RemoteException; void scheduleActivityConfigurationChanged(IBinder token) throws RemoteException; void requestPss() throws RemoteException; - void profilerControl(boolean start, String path) throws RemoteException; - + void profilerControl(boolean start, String path, ParcelFileDescriptor fd) + throws RemoteException; + void setSchedulingGroup(int group) throws RemoteException; + String descriptor = "android.app.IApplicationThread"; int SCHEDULE_PAUSE_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION; @@ -117,4 +126,7 @@ public interface IApplicationThread extends IInterface { int SCHEDULE_RELAUNCH_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+25; int REQUEST_PSS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+26; int PROFILER_CONTROL_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+27; + int SET_SCHEDULING_GROUP_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+28; + int SCHEDULE_CREATE_BACKUP_AGENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+29; + int SCHEDULE_DESTROY_BACKUP_AGENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+30; } diff --git a/core/java/android/backup/IBackupService.aidl b/core/java/android/app/IBackupAgent.aidl index 1bde8ea..9b0550f 100644 --- a/core/java/android/backup/IBackupService.aidl +++ b/core/java/android/app/IBackupAgent.aidl @@ -14,18 +14,18 @@ * limitations under the License. */ -package android.backup; +package android.app; import android.os.ParcelFileDescriptor; /** * Interface presented by applications being asked to participate in the * backup & restore mechanism. End user code does not typically implement - * this interface; they subclass BackupService instead. + * this interface; they subclass BackupAgent instead. * * {@hide} */ -interface IBackupService { +interface IBackupAgent { /** * Request that the app perform an incremental backup. * @@ -51,9 +51,14 @@ interface IBackupService { * app's backup. This is to be a <i>replacement</i> of the app's * current data, not to be merged into it. * + * @param appVersionCode The android:versionCode attribute of the application + * that created this data set. This can help the agent distinguish among + * various historical backup content possibilities. + * * @param newState Read-write file, empty when onRestore() is called, * that is to be written with the state description that holds after * the restore has been completed. */ - void doRestore(in ParcelFileDescriptor data, in ParcelFileDescriptor newState); + void doRestore(in ParcelFileDescriptor data, int appVersionCode, + in ParcelFileDescriptor newState); } diff --git a/core/java/android/app/IIntentReceiver.aidl b/core/java/android/app/IIntentReceiver.aidl deleted file mode 100755 index 5f5d0eb..0000000 --- a/core/java/android/app/IIntentReceiver.aidl +++ /dev/null @@ -1,33 +0,0 @@ -/* -** -** Copyright 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.app; - -import android.content.Intent; -import android.os.Bundle; - -/** - * System private API for dispatching intent broadcasts. This is given to the - * activity manager as part of registering for an intent broadcasts, and is - * called when it receives intents. - * - * {@hide} - */ -oneway interface IIntentReceiver { - void performReceive(in Intent intent, int resultCode, - String data, in Bundle extras, boolean ordered); -} - diff --git a/core/java/android/app/IIntentSender.aidl b/core/java/android/app/IIntentSender.aidl deleted file mode 100644 index 53e135a..0000000 --- a/core/java/android/app/IIntentSender.aidl +++ /dev/null @@ -1,27 +0,0 @@ -/* //device/java/android/android/app/IActivityPendingResult.aidl -** -** Copyright 2007, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** 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.IIntentReceiver; -import android.content.Intent; - -/** @hide */ -interface IIntentSender { - int send(int code, in Intent intent, String resolvedType, - IIntentReceiver finishedReceiver); -} diff --git a/core/java/android/app/ISearchManager.aidl b/core/java/android/app/ISearchManager.aidl index 39eb4f1..e8bd60a 100644 --- a/core/java/android/app/ISearchManager.aidl +++ b/core/java/android/app/ISearchManager.aidl @@ -16,11 +16,28 @@ package android.app; +import android.app.ISearchManagerCallback; import android.content.ComponentName; +import android.content.res.Configuration; +import android.os.Bundle; import android.server.search.SearchableInfo; /** @hide */ interface ISearchManager { SearchableInfo getSearchableInfo(in ComponentName launchActivity, boolean globalSearch); List<SearchableInfo> getSearchablesInGlobalSearch(); + List<SearchableInfo> getSearchablesForWebSearch(); + SearchableInfo getDefaultSearchableForWebSearch(); + void setDefaultWebSearch(in ComponentName component); + void startSearch(in String initialQuery, + boolean selectInitialQuery, + in ComponentName launchActivity, + in Bundle appSearchData, + boolean globalSearch, + ISearchManagerCallback searchManagerCallback); + void stopSearch(); + boolean isVisible(); + Bundle onSaveInstanceState(); + void onRestoreInstanceState(in Bundle savedInstanceState); + void onConfigurationChanged(in Configuration newConfig); } diff --git a/core/java/android/app/ISearchManagerCallback.aidl b/core/java/android/app/ISearchManagerCallback.aidl new file mode 100644 index 0000000..bdfb2ba --- /dev/null +++ b/core/java/android/app/ISearchManagerCallback.aidl @@ -0,0 +1,23 @@ +/** + * 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; + +/** @hide */ +oneway interface ISearchManagerCallback { + void onDismiss(); + void onCancel(); +} diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index f6a28b2..e31f4f8 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -446,13 +446,13 @@ public class Instrumentation { if (ai == null) { throw new RuntimeException("Unable to resolve activity for: " + intent); } - if (!ai.applicationInfo.processName.equals( - getTargetContext().getPackageName())) { + String myProc = mThread.getProcessName(); + if (!ai.processName.equals(myProc)) { // todo: if this intent is ambiguous, look here to see if // there is a single match that is in our package. - throw new RuntimeException("Intent resolved to different package " - + ai.applicationInfo.packageName + ": " - + intent); + throw new RuntimeException("Intent in process " + + myProc + " resolved to different process " + + ai.processName + ": " + intent); } intent.setComponent(new ComponentName( diff --git a/core/java/android/app/LauncherActivity.java b/core/java/android/app/LauncherActivity.java index 8d249da..accdda9 100644 --- a/core/java/android/app/LauncherActivity.java +++ b/core/java/android/app/LauncherActivity.java @@ -60,26 +60,20 @@ public abstract class LauncherActivity extends ListActivity { * An item in the list */ public static class ListItem { + public ResolveInfo resolveInfo; public CharSequence label; - //public CharSequence description; public Drawable icon; public String packageName; public String className; public Bundle extras; ListItem(PackageManager pm, ResolveInfo resolveInfo, IconResizer resizer) { + this.resolveInfo = resolveInfo; label = resolveInfo.loadLabel(pm); if (label == null && resolveInfo.activityInfo != null) { label = resolveInfo.activityInfo.name; } - /* - if (resolveInfo.activityInfo != null && - resolveInfo.activityInfo.applicationInfo != null) { - description = resolveInfo.activityInfo.applicationInfo.loadDescription(pm); - } - */ - icon = resizer.createIconThumbnail(resolveInfo.loadIcon(pm)); packageName = resolveInfo.activityInfo.applicationInfo.packageName; className = resolveInfo.activityInfo.name; @@ -122,6 +116,14 @@ public abstract class LauncherActivity extends ListActivity { return intent; } + public ListItem itemForPosition(int position) { + if (mActivitiesList == null) { + return null; + } + + return mActivitiesList.get(position); + } + public int getCount() { return mActivitiesList != null ? mActivitiesList.size() : 0; } @@ -354,6 +356,16 @@ public abstract class LauncherActivity extends ListActivity { } /** + * Return the {@link ListItem} for a specific position in our + * {@link android.widget.ListView}. + * @param position The item to return + */ + protected ListItem itemForPosition(int position) { + ActivityAdapter adapter = (ActivityAdapter) mAdapter; + return adapter.itemForPosition(position); + } + + /** * Get the base intent to use when running * {@link PackageManager#queryIntentActivities(Intent, int)}. */ diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index cb660c7..f9c38f9 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -18,6 +18,9 @@ package android.app; import android.content.Context; import android.content.Intent; +import android.content.IIntentReceiver; +import android.content.IIntentSender; +import android.content.IntentSender; import android.os.Bundle; import android.os.RemoteException; import android.os.Handler; @@ -105,7 +108,7 @@ public final class PendingIntent implements Parcelable { public CanceledException(Exception cause) { super(cause); } - }; + } /** * Callback interface for discovering when a send operation has @@ -270,6 +273,21 @@ public final class PendingIntent implements Parcelable { return null; } + private class IntentSenderWrapper extends IntentSender { + protected IntentSenderWrapper(IIntentSender target) { + super(target); + } + } + /** + * Retrieve a IntentSender object that wraps the existing sender of the PendingIntent + * + * @return Returns a IntentSender object that wraps the sender of PendingIntent + * + */ + public IntentSender getIntentSender() { + return new IntentSenderWrapper(mTarget); + } + /** * Cancel a currently active PendingIntent. Only the original application * owning an PendingIntent can cancel it. diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 343380c..fdb619a 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -24,6 +24,8 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.ContentResolver; +import android.content.ContentValues; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -32,6 +34,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Animatable; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; @@ -41,8 +44,10 @@ import android.text.Editable; import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.util.Regex; import android.util.AttributeSet; import android.util.Log; +import android.view.ContextThemeWrapper; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; @@ -51,6 +56,7 @@ import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.AutoCompleteTextView; @@ -67,8 +73,8 @@ import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicLong; /** - * In-application-process implementation of Search Bar. This is still controlled by the - * SearchManager, but it runs in the current activity's process to keep things lighter weight. + * System search dialog. This is controlled by the + * SearchManagerService and runs in the system process. * * @hide */ @@ -82,13 +88,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS private static final String INSTANCE_KEY_COMPONENT = "comp"; private static final String INSTANCE_KEY_APPDATA = "data"; private static final String INSTANCE_KEY_GLOBALSEARCH = "glob"; - private static final String INSTANCE_KEY_DISPLAY_QUERY = "dQry"; - private static final String INSTANCE_KEY_DISPLAY_SEL_START = "sel1"; - private static final String INSTANCE_KEY_DISPLAY_SEL_END = "sel2"; - private static final String INSTANCE_KEY_SELECTED_ELEMENT = "slEl"; - private static final int INSTANCE_SELECTED_BUTTON = -2; - private static final int INSTANCE_SELECTED_QUERY = -1; - + private static final String INSTANCE_KEY_STORED_COMPONENT = "sComp"; + private static final String INSTANCE_KEY_STORED_APPDATA = "sData"; + private static final String INSTANCE_KEY_PREVIOUS_COMPONENTS = "sPrev"; + private static final String INSTANCE_KEY_USER_QUERY = "uQry"; + private static final int SEARCH_PLATE_LEFT_PADDING_GLOBAL = 12; private static final int SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL = 7; @@ -103,6 +107,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS private Button mGoButton; private ImageButton mVoiceButton; private View mSearchPlate; + private Drawable mWorkingSpinner; // interaction with searchable application private SearchableInfo mSearchable; @@ -129,9 +134,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS private SuggestionsAdapter mSuggestionsAdapter; // Whether to rewrite queries when selecting suggestions - // TODO: This is disabled because of problems with persistent selections - // causing non-user-initiated rewrites. - private static final boolean REWRITE_QUERIES = false; + private static final boolean REWRITE_QUERIES = true; // The query entered by the user. This is not changed when selecting a suggestion // that modifies the contents of the text field. But if the user then edits @@ -142,14 +145,17 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // more than once. private final WeakHashMap<String, Drawable> mOutsideDrawablesCache = new WeakHashMap<String, Drawable>(); - + + // Last known IME options value for the search edit text. + private int mSearchAutoCompleteImeOptions; + /** * Constructor - fires it up and makes it look like the search UI. * * @param context Application Context we can use for system acess */ public SearchDialog(Context context) { - super(context, com.android.internal.R.style.Theme_SearchBar); + super(context, com.android.internal.R.style.Theme_GlobalSearchBar); } /** @@ -160,17 +166,17 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Window theWindow = getWindow(); - theWindow.setGravity(Gravity.TOP|Gravity.FILL_HORIZONTAL); - setContentView(com.android.internal.R.layout.search_bar); - theWindow.setLayout(ViewGroup.LayoutParams.FILL_PARENT, - // taking up the whole window (even when transparent) is less than ideal, - // but necessary to show the popup window until the window manager supports - // having windows anchored by their parent but not clipped by them. - ViewGroup.LayoutParams.FILL_PARENT); + Window theWindow = getWindow(); WindowManager.LayoutParams lp = theWindow.getAttributes(); + lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR; + lp.width = ViewGroup.LayoutParams.FILL_PARENT; + // taking up the whole window (even when transparent) is less than ideal, + // but necessary to show the popup window until the window manager supports + // having windows anchored by their parent but not clipped by them. + lp.height = ViewGroup.LayoutParams.FILL_PARENT; + lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL; lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; theWindow.setAttributes(lp); @@ -182,6 +188,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn); mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn); mSearchPlate = findViewById(com.android.internal.R.id.search_plate); + mWorkingSpinner = getContext().getResources(). + getDrawable(com.android.internal.R.drawable.search_spinner); // attach listeners mSearchAutoComplete.addTextChangedListener(mTextWatcher); @@ -213,10 +221,14 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // Save voice intent for later queries/launching mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); + mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH); mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + mSearchAutoCompleteImeOptions = mSearchAutoComplete.getImeOptions(); } /** @@ -226,20 +238,21 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS */ public boolean show(String initialQuery, boolean selectInitialQuery, ComponentName componentName, Bundle appSearchData, boolean globalSearch) { - if (isShowing()) { - // race condition - already showing but not handling events yet. - // in this case, just discard the "show" request - return true; - } - + // Reset any stored values from last time dialog was shown. mStoredComponentName = null; mStoredAppSearchData = null; - - return doShow(initialQuery, selectInitialQuery, componentName, appSearchData, globalSearch); + + boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData, + globalSearch); + if (success) { + // Display the drop down as soon as possible instead of waiting for the rest of the + // pending UI stuff to get done, so that things appear faster to the user. + mSearchAutoComplete.showDropDownAfterLayout(); + } + return success; } - /** * Called in response to a press of the hard search button in * {@link #onKeyDown(int, KeyEvent)}, this method toggles between in-app @@ -309,15 +322,17 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS + appSearchData + ", " + globalSearch + ")"); } + SearchManager searchManager = (SearchManager) + mContext.getSystemService(Context.SEARCH_SERVICE); // Try to get the searchable info for the provided component (or for global search, // if globalSearch == true). - mSearchable = SearchManager.getSearchableInfo(componentName, globalSearch); + mSearchable = searchManager.getSearchableInfo(componentName, globalSearch); // If we got back nothing, and it wasn't a request for global search, then try again // for global search, as we'll try to launch that in lieu of any component-specific search. if (!globalSearch && mSearchable == null) { globalSearch = true; - mSearchable = SearchManager.getSearchableInfo(componentName, globalSearch); + mSearchable = searchManager.getSearchableInfo(componentName, globalSearch); // If we still get back null (i.e., there's not even a searchable info available // for global search), then really give up. @@ -332,7 +347,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mAppSearchData = appSearchData; // Using globalSearch here is just an optimization, just calling // isDefaultSearchable() should always give the same result. - mGlobalSearchMode = globalSearch || SearchManager.isDefaultSearchable(mSearchable); + mGlobalSearchMode = globalSearch || searchManager.isDefaultSearchable(mSearchable); mActivityContext = mSearchable.getActivityContext(getContext()); // show the dialog. this will call onStart(). @@ -345,6 +360,21 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS getContext().getSystemService(Context.INPUT_METHOD_SERVICE); inputManager.showSoftInputUnchecked(0, null); } + + // The Dialog uses a ContextThemeWrapper for the context; use this to change the + // theme out from underneath us, between the global search theme and the in-app + // search theme. They are identical except that the global search theme does not + // dim the background of the window (because global search is full screen so it's + // not needed and this should save a little bit of time on global search invocation). + Object context = getContext(); + if (context instanceof ContextThemeWrapper) { + ContextThemeWrapper wrapper = (ContextThemeWrapper) context; + if (globalSearch) { + wrapper.setTheme(com.android.internal.R.style.Theme_GlobalSearchBar); + } else { + wrapper.setTheme(com.android.internal.R.style.Theme_SearchBar); + } + } show(); } @@ -372,11 +402,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS public void onStop() { super.onStop(); - // TODO: Removing the listeners means that they never get called, since - // Dialog.dismissDialog() calls onStop() before sendDismissMessage(). - setOnCancelListener(null); - setOnDismissListener(null); - // stop receiving broadcasts (throws exception if none registered) try { getContext().unregisterReceiver(mBroadcastReceiver); @@ -394,6 +419,24 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mUserQuery = null; mPreviousComponents = null; } + + /** + * Sets the search dialog to the 'working' state, which shows a working spinner in the + * right hand size of the text field. + * + * @param working true to show spinner, false to hide spinner + */ + public void setWorking(boolean working) { + if (working) { + mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( + null, null, mWorkingSpinner, null); + ((Animatable) mWorkingSpinner).start(); + } else { + mSearchAutoComplete.setCompoundDrawablesWithIntrinsicBounds( + null, null, null, null); + ((Animatable) mWorkingSpinner).stop(); + } + } /** * Closes and gets rid of the suggestions adapter. @@ -412,8 +455,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS /** * Save the minimal set of data necessary to recreate the search * - * TODO: go through this and make sure that it saves everything that is needed - * * @return A bundle with the state of the dialog. */ @Override @@ -424,20 +465,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent); bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData); bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode); - - // UI state - bundle.putString(INSTANCE_KEY_DISPLAY_QUERY, mSearchAutoComplete.getText().toString()); - bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_START, mSearchAutoComplete.getSelectionStart()); - bundle.putInt(INSTANCE_KEY_DISPLAY_SEL_END, mSearchAutoComplete.getSelectionEnd()); - - int selectedElement = INSTANCE_SELECTED_QUERY; - if (mGoButton.isFocused()) { - selectedElement = INSTANCE_SELECTED_BUTTON; - } else if (mSearchAutoComplete.isPopupShowing()) { - selectedElement = 0; // TODO mSearchTextField.getListSelection() // 0..n - } - bundle.putInt(INSTANCE_KEY_SELECTED_ELEMENT, selectedElement); - + bundle.putParcelable(INSTANCE_KEY_STORED_COMPONENT, mStoredComponentName); + bundle.putBundle(INSTANCE_KEY_STORED_APPDATA, mStoredAppSearchData); + bundle.putParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS, mPreviousComponents); + bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); + return bundle; } @@ -451,45 +483,27 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS */ @Override public void onRestoreInstanceState(Bundle savedInstanceState) { - // Get the launch info ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT); Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA); boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH); - - // get the UI state - String displayQuery = savedInstanceState.getString(INSTANCE_KEY_DISPLAY_QUERY); - int querySelStart = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_START, -1); - int querySelEnd = savedInstanceState.getInt(INSTANCE_KEY_DISPLAY_SEL_END, -1); - int selectedElement = savedInstanceState.getInt(INSTANCE_KEY_SELECTED_ELEMENT); - - // show the dialog. skip any show/hide animation, we want to go fast. - // send the text that actually generates the suggestions here; we'll replace the display - // text as necessary in a moment. - if (!show(displayQuery, false, launchComponent, appSearchData, globalSearch)) { + ComponentName storedComponentName = + savedInstanceState.getParcelable(INSTANCE_KEY_STORED_COMPONENT); + Bundle storedAppSearchData = + savedInstanceState.getBundle(INSTANCE_KEY_STORED_APPDATA); + ArrayList<ComponentName> previousComponents = + savedInstanceState.getParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS); + String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); + + // Set stored state + mStoredComponentName = storedComponentName; + mStoredAppSearchData = storedAppSearchData; + mPreviousComponents = previousComponents; + + // show the dialog. + if (!doShow(userQuery, false, launchComponent, appSearchData, globalSearch)) { // for some reason, we couldn't re-instantiate return; } - - mSearchAutoComplete.setText(displayQuery); - - // clean up the selection state - switch (selectedElement) { - case INSTANCE_SELECTED_BUTTON: - mGoButton.setEnabled(true); - mGoButton.setFocusable(true); - mGoButton.requestFocus(); - break; - case INSTANCE_SELECTED_QUERY: - if (querySelStart >= 0 && querySelEnd >= 0) { - mSearchAutoComplete.requestFocus(); - mSearchAutoComplete.setSelection(querySelStart, querySelEnd); - } - break; - default: - // TODO: defer selecting a list element until suggestion list appears -// mSearchAutoComplete.setListSelection(selectedElement) - break; - } } /** @@ -534,7 +548,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } } mSearchAutoComplete.setInputType(inputType); - mSearchAutoComplete.setImeOptions(mSearchable.getImeOptions()); + mSearchAutoCompleteImeOptions = mSearchable.getImeOptions(); + mSearchAutoComplete.setImeOptions(mSearchAutoCompleteImeOptions); } } @@ -547,24 +562,20 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mSearchAutoComplete.setDropDownAnimationStyle(0); // no animation mSearchAutoComplete.setThreshold(mSearchable.getSuggestThreshold()); + // we dismiss the entire dialog instead + mSearchAutoComplete.setDropDownDismissedOnCompletion(false); if (mGlobalSearchMode) { mSearchAutoComplete.setDropDownAlwaysVisible(true); // fill space until results come in - mSearchAutoComplete.setDropDownDismissedOnCompletion(false); - mSearchAutoComplete.setDropDownBackgroundResource( - com.android.internal.R.drawable.search_dropdown_background); } else { mSearchAutoComplete.setDropDownAlwaysVisible(false); - mSearchAutoComplete.setDropDownDismissedOnCompletion(true); - mSearchAutoComplete.setDropDownBackgroundResource( - com.android.internal.R.drawable.search_dropdown_background_apps); } // attach the suggestions adapter, if suggestions are available // The existence of a suggestions authority is the proxy for "suggestions available here" if (mSearchable.getSuggestAuthority() != null) { - mSuggestionsAdapter = new SuggestionsAdapter(getContext(), mSearchable, - mOutsideDrawablesCache); + mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable, + mOutsideDrawablesCache, mGlobalSearchMode); mSearchAutoComplete.setAdapter(mSuggestionsAdapter); } } @@ -597,7 +608,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mSearchPlate.getPaddingBottom()); } else { PackageManager pm = getContext().getPackageManager(); - Drawable icon = null; + Drawable icon; try { ActivityInfo info = pm.getActivityInfo(mLaunchComponent, 0); icon = pm.getApplicationIcon(info.applicationInfo); @@ -765,7 +776,24 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } } - public void afterTextChanged(Editable s) { } + public void afterTextChanged(Editable s) { + if (!mSearchAutoComplete.isPerformingCompletion()) { + // The user changed the query, check if it is a URL and if so change the search + // button in the soft keyboard to the 'Go' button. + int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION)); + if (Regex.WEB_URL_PATTERN.matcher(mUserQuery).matches()) { + options = options | EditorInfo.IME_ACTION_GO; + } else { + options = options | EditorInfo.IME_ACTION_SEARCH; + } + if (options != mSearchAutoCompleteImeOptions) { + mSearchAutoCompleteImeOptions = options; + mSearchAutoComplete.setImeOptions(options); + // This call is required to update the soft keyboard UI with latest IME flags. + mSearchAutoComplete.setInputType(mSearchAutoComplete.getInputType()); + } + } + } }; /** @@ -903,6 +931,32 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } /** + * Corrects http/https typo errors in the given url string, and if the protocol specifier was + * not present defaults to http. + * + * @param inUrl URL to check and fix + * @return fixed URL string. + */ + private String fixUrl(String inUrl) { + if (inUrl.startsWith("http://") || inUrl.startsWith("https://")) + return inUrl; + + if (inUrl.startsWith("http:") || inUrl.startsWith("https:")) { + if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) { + inUrl = inUrl.replaceFirst("/", "//"); + } else { + inUrl = inUrl.replaceFirst(":", "://"); + } + } + + if (inUrl.indexOf("://") == -1) { + inUrl = "http://" + inUrl; + } + + return inUrl; + } + + /** * React to the user typing "enter" or other hardwired keys while typing in the search box. * This handles these special keys while the edit box has focus. */ @@ -932,7 +986,19 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { v.cancelLongPress(); - launchQuerySearch(); + + // If this is a url entered by the user and we displayed the 'Go' button which + // the user clicked, launch the url instead of using it as a search query. + if ((mSearchAutoCompleteImeOptions & EditorInfo.IME_MASK_ACTION) + == EditorInfo.IME_ACTION_GO) { + Uri uri = Uri.parse(fixUrl(mSearchAutoComplete.getText().toString())); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + launchIntent(intent); + } else { + // Launch as a regular search. + launchQuerySearch(); + } return true; } if (event.getAction() == KeyEvent.ACTION_DOWN) { @@ -1069,7 +1135,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS */ protected void launchQuerySearch(int actionKey, String actionMsg) { String query = mSearchAutoComplete.getText().toString(); - Intent intent = createIntent(Intent.ACTION_SEARCH, null, query, null, + Intent intent = createIntent(Intent.ACTION_SEARCH, null, null, query, null, actionKey, actionMsg); launchIntent(intent); } @@ -1097,15 +1163,121 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS protected boolean launchSuggestion(int position, int actionKey, String actionMsg) { Cursor c = mSuggestionsAdapter.getCursor(); if ((c != null) && c.moveToPosition(position)) { + Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); + + // report back about the click + if (mGlobalSearchMode) { + // in global search mode, do it via cursor + mSuggestionsAdapter.callCursorOnClick(c, position); + } else if (intent != null + && mPreviousComponents != null + && !mPreviousComponents.isEmpty()) { + // in-app search (and we have pivoted in as told by mPreviousComponents, + // which is used for keeping track of what we pop back to when we are pivoting into + // in app search.) + reportInAppClickToGlobalSearch(c, intent); + } + + // launch the intent launchIntent(intent); + return true; } return false; } - + + /** + * Report a click from an in app search result back to global search for shortcutting porpoises. + * + * @param c The cursor that is pointing to the clicked position. + * @param intent The intent that will be launched for the click. + */ + private void reportInAppClickToGlobalSearch(Cursor c, Intent intent) { + // for in app search, still tell global search via content provider + Uri uri = getClickReportingUri(); + final ContentValues cv = new ContentValues(); + cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_QUERY, mUserQuery); + final ComponentName source = mSearchable.getSearchActivity(); + cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_COMPONENT, source.flattenToShortString()); + + // grab the intent columns from the intent we created since it has additional + // logic for falling back on the searchable default + cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction()); + cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString()); + cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME, + intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY)); + + // ensure the icons will work for global search + cv.put(SearchManager.SUGGEST_COLUMN_ICON_1, + wrapIconForPackage( + source, + getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1))); + cv.put(SearchManager.SUGGEST_COLUMN_ICON_2, + wrapIconForPackage( + source, + getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2))); + + // the rest can be passed through directly + cv.put(SearchManager.SUGGEST_COLUMN_FORMAT, + getColumnString(c, SearchManager.SUGGEST_COLUMN_FORMAT)); + cv.put(SearchManager.SUGGEST_COLUMN_TEXT_1, + getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_1)); + cv.put(SearchManager.SUGGEST_COLUMN_TEXT_2, + getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_2)); + cv.put(SearchManager.SUGGEST_COLUMN_QUERY, + getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY)); + cv.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, + getColumnString(c, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID)); + // note: deliberately omitting background color since it is only for global search + // "more results" entries + mContext.getContentResolver().insert(uri, cv); + } + /** - * Launches an intent. Also dismisses the search dialog if not in global search mode. + * @return A URI appropriate for reporting a click. + */ + private Uri getClickReportingUri() { + Uri.Builder uriBuilder = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SearchManager.SEARCH_CLICK_REPORT_AUTHORITY); + + uriBuilder.appendPath(SearchManager.SEARCH_CLICK_REPORT_URI_PATH); + + return uriBuilder + .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() + .fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() + .build(); + } + + /** + * Wraps an icon for a particular package. If the icon is a resource id, it is converted into + * an android.resource:// URI. + * + * @param source The source of the icon + * @param icon The icon retrieved from a suggestion column + * @return An icon string appropriate for the package. + */ + private String wrapIconForPackage(ComponentName source, String icon) { + if (icon == null || icon.length() == 0 || "0".equals(icon)) { + // SearchManager specifies that null or zero can be returned to indicate + // no icon. We also allow empty string. + return null; + } else if (!Character.isDigit(icon.charAt(0))){ + return icon; + } else { + String packageName = source.getPackageName(); + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(packageName) + .encodedPath(icon) + .toString(); + } + } + + /** + * Launches an intent and dismisses the search dialog (unless the intent + * is one of the special intents that modifies the state of the search dialog). */ private void launchIntent(Intent intent) { if (intent == null) { @@ -1114,9 +1286,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (handleSpecialIntent(intent)){ return; } - if (!mGlobalSearchMode) { - dismiss(); - } + dismiss(); getContext().startActivity(intent); } @@ -1130,15 +1300,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) { handleChangeSourceIntent(intent); return true; - } else if (SearchManager.INTENT_ACTION_CURSOR_RESPOND.equals(action)) { - handleCursorRespondIntent(intent); - return true; } return false; } /** - * Handles SearchManager#INTENT_ACTION_CHANGE_SOURCE. + * Handles {@link SearchManager#INTENT_ACTION_CHANGE_SEARCH_SOURCE}. */ private void handleChangeSourceIntent(Intent intent) { Uri dataUri = intent.getData(); @@ -1162,18 +1329,16 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS String query = intent.getStringExtra(SearchManager.QUERY); setUserQuery(query); + mSearchAutoComplete.showDropDown(); } - + /** - * Handles {@link SearchManager#INTENT_ACTION_CURSOR_RESPOND}. + * Sets the list item selection in the AutoCompleteTextView's ListView. */ - private void handleCursorRespondIntent(Intent intent) { - Cursor c = mSuggestionsAdapter.getCursor(); - if (c != null) { - c.respond(intent.getExtras()); - } + public void setListSelection(int index) { + mSearchAutoComplete.setListSelection(index); } - + /** * Saves the previous component that was searched, so that we can go * back to it. @@ -1243,6 +1408,12 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS try { // use specific action if supplied, or default action if supplied, or fixed default String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION); + + // some items are display only, or have effect via the cursor respond click reporting. + if (SearchManager.INTENT_ACTION_NONE.equals(action)) { + return null; + } + if (action == null) { action = mSearchable.getSuggestIntentAction(); } @@ -1264,11 +1435,14 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } Uri dataUri = (data == null) ? null : Uri.parse(data); - String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); - + String componentName = getColumnString( + c, SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME); + String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); + String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); - return createIntent(action, dataUri, query, extraData, actionKey, actionMsg); + return createIntent(action, dataUri, extraData, query, componentName, actionKey, + actionMsg); } catch (RuntimeException e ) { int rowNum; try { // be really paranoid now @@ -1287,27 +1461,33 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * * @param action Intent action. * @param data Intent data, or <code>null</code>. - * @param query Intent query, or <code>null</code>. * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>. + * @param query Intent query, or <code>null</code>. + * @param componentName Data for {@link SearchManager#COMPONENT_NAME_KEY} or <code>null</code>. * @param actionKey The key code of the action key that was pressed, * or {@link KeyEvent#KEYCODE_UNKNOWN} if none. * @param actionMsg The message for the action key that was pressed, * or <code>null</code> if none. * @return The intent. */ - private Intent createIntent(String action, Uri data, String query, String extraData, - int actionKey, String actionMsg) { + private Intent createIntent(String action, Uri data, String extraData, String query, + String componentName, int actionKey, String actionMsg) { // Now build the Intent Intent intent = new Intent(action); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (data != null) { intent.setData(data); } + intent.putExtra(SearchManager.USER_QUERY, mUserQuery); if (query != null) { intent.putExtra(SearchManager.QUERY, query); } if (extraData != null) { intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); } + if (componentName != null) { + intent.putExtra(SearchManager.COMPONENT_NAME_KEY, componentName); + } if (mAppSearchData != null) { intent.putExtra(SearchManager.APP_DATA, mAppSearchData); } @@ -1383,20 +1563,22 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS private boolean isEmpty() { return TextUtils.getTrimmedLength(getText()) == 0; } - + /** - * Clears the entered text. + * We override this method to avoid replacing the query box text + * when a suggestion is clicked. */ - private void clear() { - setText(""); + @Override + protected void replaceText(CharSequence text) { } /** - * We override this method to avoid replacing the query box text - * when a suggestion is clicked. + * We override this method to avoid an extra onItemClick being called on the + * drop-down's OnItemClickListener by {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} + * when an item is clicked with the trackball. */ @Override - protected void replaceText(CharSequence text) { + public void performCompletion() { } /** diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index 3bf37c3..e5ba6a4 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -28,6 +28,7 @@ import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; import android.server.search.SearchableInfo; +import android.util.Log; import android.view.KeyEvent; import java.util.List; @@ -1108,6 +1109,10 @@ import java.util.List; public class SearchManager implements DialogInterface.OnDismissListener, DialogInterface.OnCancelListener { + + private static final boolean DBG = false; + private static final String TAG = "SearchManager"; + /** * This is a shortcut definition for the default menu key to use for invoking search. * @@ -1131,6 +1136,20 @@ public class SearchManager public final static String QUERY = "query"; /** + * Intent extra data key: Use this key with + * {@link android.content.Intent#getStringExtra + * content.Intent.getStringExtra()} + * to obtain the query string typed in by the user. + * This may be different from the value of {@link #QUERY} + * if the intent is the result of selecting a suggestion. + * In that case, {@link #QUERY} will contain the value of + * {@link #SUGGEST_COLUMN_QUERY} for the suggestion, and + * {@link #USER_QUERY} will contain the string typed by the + * user. + */ + public final static String USER_QUERY = "user_query"; + + /** * Intent extra data key: Use this key with Intent.ACTION_SEARCH and * {@link android.content.Intent#getBundleExtra * content.Intent.getBundleExtra()} @@ -1148,7 +1167,7 @@ public class SearchManager * @hide */ public final static String SOURCE = "source"; - + /** * Intent extra data key: Use this key with Intent.ACTION_SEARCH and * {@link android.content.Intent#getIntExtra content.Intent.getIntExtra()} @@ -1160,12 +1179,66 @@ public class SearchManager public final static String ACTION_KEY = "action_key"; /** + * Intent component name key: This key will be used for the extra populated by the + * {@link #SUGGEST_COLUMN_INTENT_COMPONENT_NAME} column. + * + * {@hide} + */ + public final static String COMPONENT_NAME_KEY = "intent_component_name_key"; + + /** * Intent extra data key: This key will be used for the extra populated by the * {@link #SUGGEST_COLUMN_INTENT_EXTRA_DATA} column. + * * {@hide} */ public final static String EXTRA_DATA_KEY = "intent_extra_data_key"; - + + /** + * Defines the constants used in the communication between {@link android.app.SearchDialog} and + * the global search provider via {@link Cursor#respond(android.os.Bundle)}. + * + * @hide + */ + public static class DialogCursorProtocol { + + /** + * The sent bundle will contain this integer key, with a value set to one of the events + * below. + */ + public final static String METHOD = "DialogCursorProtocol.method"; + + /** + * After data has been refreshed. + */ + public final static int POST_REFRESH = 0; + public final static String POST_REFRESH_RECEIVE_ISPENDING + = "DialogCursorProtocol.POST_REFRESH.isPending"; + public final static String POST_REFRESH_RECEIVE_DISPLAY_NOTIFY + = "DialogCursorProtocol.POST_REFRESH.displayNotify"; + + /** + * Just before closing the cursor. + */ + public final static int PRE_CLOSE = 1; + public final static String PRE_CLOSE_SEND_MAX_DISPLAY_POS + = "DialogCursorProtocol.PRE_CLOSE.sendDisplayPosition"; + + /** + * When a position has been clicked. + */ + public final static int CLICK = 2; + public final static String CLICK_SEND_POSITION + = "DialogCursorProtocol.CLICK.sendPosition"; + public final static String CLICK_RECEIVE_SELECTED_POS + = "DialogCursorProtocol.CLICK.receiveSelectedPosition"; + + /** + * When the threshold received in {@link #POST_REFRESH_RECEIVE_DISPLAY_NOTIFY} is displayed. + */ + public final static int THRESH_HIT = 3; + } + /** * Intent extra data key: Use this key with Intent.ACTION_SEARCH and * {@link android.content.Intent#getStringExtra content.Intent.getStringExtra()} @@ -1210,6 +1283,41 @@ public class SearchManager */ public final static String SHORTCUT_MIME_TYPE = "vnd.android.cursor.item/vnd.android.search.suggest"; + + + /** + * The authority of the provider to report clicks to when a click is detected after pivoting + * into a specific app's search from global search. + * + * In addition to the columns below, the suggestion columns are used to pass along the full + * suggestion so it can be shortcutted. + * + * @hide + */ + public final static String SEARCH_CLICK_REPORT_AUTHORITY = + "com.android.globalsearch.stats"; + + /** + * The path the write goes to. + * + * @hide + */ + public final static String SEARCH_CLICK_REPORT_URI_PATH = "click"; + + /** + * The column storing the query for the click. + * + * @hide + */ + public final static String SEARCH_CLICK_REPORT_COLUMN_QUERY = "query"; + + /** + * The column storing the component name of the application that was pivoted into. + * + * @hide + */ + public final static String SEARCH_CLICK_REPORT_COLUMN_COMPONENT = "component"; + /** * Column name for suggestions cursor. <i>Unused - can be null or column can be omitted.</i> */ @@ -1258,28 +1366,6 @@ public class SearchManager */ public final static String SUGGEST_COLUMN_ICON_2 = "suggest_icon_2"; /** - * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column, - * then all suggestions will be provided in a format that includes space for two small icons, - * one at the left and one at the right of each suggestion. The data in the column must - * be a blob that contains a bitmap. - * - * This column overrides any icon provided in the {@link #SUGGEST_COLUMN_ICON_1} column. - * - * @hide - */ - public final static String SUGGEST_COLUMN_ICON_1_BITMAP = "suggest_icon_1_bitmap"; - /** - * Column name for suggestions cursor. <i>Optional.</i> If your cursor includes this column, - * then all suggestions will be provided in a format that includes space for two small icons, - * one at the left and one at the right of each suggestion. The data in the column must - * be a blob that contains a bitmap. - * - * This column overrides any icon provided in the {@link #SUGGEST_COLUMN_ICON_2} column. - * - * @hide - */ - public final static String SUGGEST_COLUMN_ICON_2_BITMAP = "suggest_icon_2_bitmap"; - /** * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i> * this element exists at the given row, this is the action that will be used when * forming the suggestion's intent. If the element is not provided, the action will be taken @@ -1300,13 +1386,24 @@ public class SearchManager */ public final static String SUGGEST_COLUMN_INTENT_DATA = "suggest_intent_data"; /** + * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i> + * this element exists at the given row, this is the data that will be used when + * forming the suggestion's intent. If not provided, the Intent's extra data field will be null. + * This column allows suggestions to provide additional arbitrary data which will be included as + * an extra under the key EXTRA_DATA_KEY. + * + * @hide Pending API council approval. + */ + public final static String SUGGEST_COLUMN_INTENT_EXTRA_DATA = "suggest_intent_extra_data"; + /** * Column name for suggestions cursor. <i>Optional.</i> This column allows suggestions * to provide additional arbitrary data which will be included as an extra under the key - * {@link #EXTRA_DATA_KEY}. - * - * @hide pending API council approval + * {@link #COMPONENT_NAME_KEY}. For use by the global search system only - if other providers + * attempt to use this column, the value will be overwritten by global search. + * + * @hide */ - public final static String SUGGEST_COLUMN_INTENT_EXTRA_DATA = "suggest_intent_extra_data"; + public final static String SUGGEST_COLUMN_INTENT_COMPONENT_NAME = "suggest_intent_component"; /** * Column name for suggestions cursor. <i>Optional.</i> If this column exists <i>and</i> * this element exists at the given row, then "/" and this value will be appended to the data @@ -1335,6 +1432,25 @@ public class SearchManager public final static String SUGGEST_COLUMN_SHORTCUT_ID = "suggest_shortcut_id"; /** + * Column name for suggestions cursor. <i>Optional.</i> This column is used to specify the + * cursor item's background color if it needs a non-default background color. A non-zero value + * indicates a valid background color to override the default. + * + * @hide For internal use, not part of the public API. + */ + public final static String SUGGEST_COLUMN_BACKGROUND_COLOR = "suggest_background_color"; + + /** + * Column name for suggestions cursor. <i>Optional.</i> This column is used to specify + * that a spinner should be shown in lieu of an icon2 while the shortcut of this suggestion + * is being refreshed. + * + * @hide Pending API council approval. + */ + public final static String SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING = + "suggest_spinner_while_refreshing"; + + /** * Column value for suggestion column {@link #SUGGEST_COLUMN_SHORTCUT_ID} when a suggestion * should not be stored as a shortcut in global search. * @@ -1362,21 +1478,7 @@ public class SearchManager */ public final static String INTENT_ACTION_CHANGE_SEARCH_SOURCE = "android.search.action.CHANGE_SEARCH_SOURCE"; - - /** - * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION}, - * the search dialog will call {@link Cursor#respond(Bundle)} when the - * suggestion is clicked. - * - * The {@link Bundle} argument will be constructed - * in the same way as the "extra" bundle included in an Intent constructed - * from the suggestion. - * - * @hide Pending API council approval. - */ - public final static String INTENT_ACTION_CURSOR_RESPOND - = "android.search.action.CURSOR_RESPOND"; - + /** * Intent action for finding the global search activity. * The global search provider should handle this intent. @@ -1396,21 +1498,53 @@ public class SearchManager = "android.search.action.SEARCH_SETTINGS"; /** + * Intent action for starting a web search provider's settings activity. + * Web search providers should handle this intent if they have provider-specific + * settings to implement. + * + * @hide Pending API council approval. + */ + public final static String INTENT_ACTION_WEB_SEARCH_SETTINGS + = "android.search.action.WEB_SEARCH_SETTINGS"; + + /** + * Intent action broadcasted to inform that the searchables list or default have changed. + * Components should handle this intent if they cache any searchable data and wish to stay + * up to date on changes. + * + * @hide Pending API council approval. + */ + public final static String INTENT_ACTION_SEARCHABLES_CHANGED + = "android.search.action.SEARCHABLES_CHANGED"; + + /** + * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION}, + * the search dialog will take no action. + * + * @hide + */ + public final static String INTENT_ACTION_NONE = "android.search.action.ZILCH"; + + /** * Reference to the shared system search service. */ - private static ISearchManager sService = getSearchManagerService(); + private static ISearchManager mService; private final Context mContext; - private final Handler mHandler; - - private SearchDialog mSearchDialog; - - private OnDismissListener mDismissListener = null; - private OnCancelListener mCancelListener = null; + + // package private since they are used by the inner class SearchManagerCallback + /* package */ boolean mIsShowing = false; + /* package */ final Handler mHandler; + /* package */ OnDismissListener mDismissListener = null; + /* package */ OnCancelListener mCancelListener = null; + + private final SearchManagerCallback mSearchManagerCallback = new SearchManagerCallback(); /*package*/ SearchManager(Context context, Handler handler) { mContext = context; mHandler = handler; + mService = ISearchManager.Stub.asInterface( + ServiceManager.getService(Context.SEARCH_SERVICE)); } /** @@ -1458,17 +1592,16 @@ public class SearchManager ComponentName launchActivity, Bundle appSearchData, boolean globalSearch) { - - if (mSearchDialog == null) { - mSearchDialog = new SearchDialog(mContext); + if (DBG) debug("startSearch(), mIsShowing=" + mIsShowing); + if (mIsShowing) return; + try { + mIsShowing = true; + // activate the search manager and start it up! + mService.startSearch(initialQuery, selectInitialQuery, launchActivity, appSearchData, + globalSearch, mSearchManagerCallback); + } catch (RemoteException ex) { + Log.e(TAG, "startSearch() failed: " + ex); } - - // activate the search manager and start it up! - mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData, - globalSearch); - - mSearchDialog.setOnCancelListener(this); - mSearchDialog.setOnDismissListener(this); } /** @@ -1482,9 +1615,16 @@ public class SearchManager * * @see #startSearch */ - public void stopSearch() { - if (mSearchDialog != null) { - mSearchDialog.cancel(); + public void stopSearch() { + if (DBG) debug("stopSearch(), mIsShowing=" + mIsShowing); + if (!mIsShowing) return; + try { + mService.stopSearch(); + // onDismiss will also clear this, but we do it here too since onDismiss() is + // called asynchronously. + mIsShowing = false; + } catch (RemoteException ex) { + Log.e(TAG, "stopSearch() failed: " + ex); } } @@ -1497,33 +1637,33 @@ public class SearchManager * * @hide */ - public boolean isVisible() { - if (mSearchDialog != null) { - return mSearchDialog.isShowing(); - } - return false; + public boolean isVisible() { + if (DBG) debug("isVisible(), mIsShowing=" + mIsShowing); + return mIsShowing; } - + /** - * See {@link #setOnDismissListener} for configuring your activity to monitor search UI state. + * See {@link SearchManager#setOnDismissListener} for configuring your activity to monitor + * search UI state. */ public interface OnDismissListener { /** - * This method will be called when the search UI is dismissed. To make use if it, you must - * implement this method in your activity, and call {@link #setOnDismissListener} to - * register it. + * This method will be called when the search UI is dismissed. To make use of it, you must + * implement this method in your activity, and call + * {@link SearchManager#setOnDismissListener} to register it. */ public void onDismiss(); } /** - * See {@link #setOnCancelListener} for configuring your activity to monitor search UI state. + * See {@link SearchManager#setOnCancelListener} for configuring your activity to monitor + * search UI state. */ public interface OnCancelListener { /** * This method will be called when the search UI is canceled. To make use if it, you must - * implement this method in your activity, and call {@link #setOnCancelListener} to - * register it. + * implement this method in your activity, and call + * {@link SearchManager#setOnCancelListener} to register it. */ public void onCancel(); } @@ -1536,84 +1676,112 @@ public class SearchManager public void setOnDismissListener(final OnDismissListener listener) { mDismissListener = listener; } - - /** - * The callback from the search dialog when dismissed - * @hide - */ - public void onDismiss(DialogInterface dialog) { - if (dialog == mSearchDialog) { - if (mDismissListener != null) { - mDismissListener.onDismiss(); - } - } - } /** * Set or clear the callback that will be invoked whenever the search UI is canceled. * * @param listener The {@link OnCancelListener} to use, or null. */ - public void setOnCancelListener(final OnCancelListener listener) { + public void setOnCancelListener(OnCancelListener listener) { mCancelListener = listener; } - - - /** - * The callback from the search dialog when canceled - * @hide - */ - public void onCancel(DialogInterface dialog) { - if (dialog == mSearchDialog) { - if (mCancelListener != null) { - mCancelListener.onCancel(); + + private class SearchManagerCallback extends ISearchManagerCallback.Stub { + + private final Runnable mFireOnDismiss = new Runnable() { + public void run() { + if (DBG) debug("mFireOnDismiss"); + mIsShowing = false; + if (mDismissListener != null) { + mDismissListener.onDismiss(); + } + } + }; + + private final Runnable mFireOnCancel = new Runnable() { + public void run() { + if (DBG) debug("mFireOnCancel"); + // doesn't need to clear mIsShowing since onDismiss() always gets called too + if (mCancelListener != null) { + mCancelListener.onCancel(); + } } + }; + + public void onDismiss() { + if (DBG) debug("onDismiss()"); + mHandler.post(mFireOnDismiss); + } + + public void onCancel() { + if (DBG) debug("onCancel()"); + mHandler.post(mFireOnCancel); } + + } + + // TODO: remove the DialogInterface interfaces from SearchManager. + // This changes the public API, so I'll do it in a separate change. + public void onCancel(DialogInterface dialog) { + throw new UnsupportedOperationException(); + } + public void onDismiss(DialogInterface dialog) { + throw new UnsupportedOperationException(); } /** - * Save instance state so we can recreate after a rotation. - * + * Saves the state of the search UI. + * + * @return A Bundle containing the state of the search dialog, or {@code null} + * if the search UI is not visible. + * * @hide */ - void saveSearchDialog(Bundle outState, String key) { - if (mSearchDialog != null && mSearchDialog.isShowing()) { - Bundle searchDialogState = mSearchDialog.onSaveInstanceState(); - outState.putBundle(key, searchDialogState); + public Bundle saveSearchDialog() { + if (DBG) debug("saveSearchDialog(), mIsShowing=" + mIsShowing); + if (!mIsShowing) return null; + try { + return mService.onSaveInstanceState(); + } catch (RemoteException ex) { + Log.e(TAG, "onSaveInstanceState() failed: " + ex); + return null; } } /** - * Restore instance state after a rotation. - * + * Restores the state of the search dialog. + * + * @param searchDialogState Bundle to read the state from. + * * @hide */ - void restoreSearchDialog(Bundle inState, String key) { - Bundle searchDialogState = inState.getBundle(key); - if (searchDialogState != null) { - if (mSearchDialog == null) { - mSearchDialog = new SearchDialog(mContext); - } - mSearchDialog.onRestoreInstanceState(searchDialogState); + public void restoreSearchDialog(Bundle searchDialogState) { + if (DBG) debug("restoreSearchDialog(" + searchDialogState + ")"); + if (searchDialogState == null) return; + try { + mService.onRestoreInstanceState(searchDialogState); + } catch (RemoteException ex) { + Log.e(TAG, "onRestoreInstanceState() failed: " + ex); } } - + /** - * Hook for updating layout on a rotation - * + * Update the search dialog after a configuration change. + * + * @param newConfig The new configuration. + * * @hide */ - void onConfigurationChanged(Configuration newConfig) { - if (mSearchDialog != null && mSearchDialog.isShowing()) { - mSearchDialog.onConfigurationChanged(newConfig); + public void onConfigurationChanged(Configuration newConfig) { + if (DBG) debug("onConfigurationChanged(" + newConfig + "), mIsShowing=" + mIsShowing); + if (!mIsShowing) return; + try { + mService.onConfigurationChanged(newConfig); + } catch (RemoteException ex) { + Log.e(TAG, "onConfigurationChanged() failed:" + ex); } } - - private static ISearchManager getSearchManagerService() { - return ISearchManager.Stub.asInterface( - ServiceManager.getService(Context.SEARCH_SERVICE)); - } - + /** * Gets information about a searchable activity. This method is static so that it can * be used from non-Activity contexts. @@ -1625,11 +1793,12 @@ public class SearchManager * * @hide because SearchableInfo is not part of the API. */ - public static SearchableInfo getSearchableInfo(ComponentName componentName, + public SearchableInfo getSearchableInfo(ComponentName componentName, boolean globalSearch) { try { - return sService.getSearchableInfo(componentName, globalSearch); - } catch (RemoteException e) { + return mService.getSearchableInfo(componentName, globalSearch); + } catch (RemoteException ex) { + Log.e(TAG, "getSearchableInfo() failed: " + ex); return null; } } @@ -1639,23 +1808,22 @@ public class SearchManager * * @hide because SearchableInfo is not part of the API. */ - public static boolean isDefaultSearchable(SearchableInfo searchable) { - SearchableInfo defaultSearchable = SearchManager.getSearchableInfo(null, true); + public boolean isDefaultSearchable(SearchableInfo searchable) { + SearchableInfo defaultSearchable = getSearchableInfo(null, true); return defaultSearchable != null && defaultSearchable.getSearchActivity().equals(searchable.getSearchActivity()); } - + /** - * Gets a cursor with search suggestions. This method is static so that it can - * be used from non-Activity context. + * Gets a cursor with search suggestions. * * @param searchable Information about how to get the suggestions. * @param query The search text entered (so far). - * @return a cursor with suggestions, or <code>null</null> the suggestion query failed. - * + * @return a cursor with suggestions, or <code>null</null> the suggestion query failed. + * * @hide because SearchableInfo is not part of the API. */ - public static Cursor getSuggestions(Context context, SearchableInfo searchable, String query) { + public Cursor getSuggestions(SearchableInfo searchable, String query) { if (searchable == null) { return null; } @@ -1694,7 +1862,7 @@ public class SearchManager .build(); // finally, make the query - return context.getContentResolver().query(uri, null, selection, selArgs, null); + return mContext.getContentResolver().query(uri, null, selection, selArgs, null); } /** @@ -1706,11 +1874,65 @@ public class SearchManager * * @hide because SearchableInfo is not part of the API. */ - public static List<SearchableInfo> getSearchablesInGlobalSearch() { + public List<SearchableInfo> getSearchablesInGlobalSearch() { try { - return sService.getSearchablesInGlobalSearch(); + return mService.getSearchablesInGlobalSearch(); } catch (RemoteException e) { + Log.e(TAG, "getSearchablesInGlobalSearch() failed: " + e); return null; } } + + /** + * Returns a list of the searchable activities that handle web searches. + * + * @return a list of all searchable activities that handle + * {@link android.content.Intent#ACTION_WEB_SEARCH}. + * + * @hide because SearchableInfo is not part of the API. + */ + public List<SearchableInfo> getSearchablesForWebSearch() { + try { + return mService.getSearchablesForWebSearch(); + } catch (RemoteException e) { + Log.e(TAG, "getSearchablesForWebSearch() failed: " + e); + return null; + } + } + + /** + * Returns the default searchable activity for web searches. + * + * @return searchable information for the activity handling web searches by default. + * + * @hide because SearchableInfo is not part of the API. + */ + public SearchableInfo getDefaultSearchableForWebSearch() { + try { + return mService.getDefaultSearchableForWebSearch(); + } catch (RemoteException e) { + Log.e(TAG, "getDefaultSearchableForWebSearch() failed: " + e); + return null; + } + } + + /** + * Sets the default searchable activity for web searches. + * + * @param component Name of the component to set as default activity for web searches. + * + * @hide + */ + public void setDefaultWebSearch(ComponentName component) { + try { + mService.setDefaultWebSearch(component); + } catch (RemoteException e) { + Log.e(TAG, "setDefaultWebSearch() failed: " + e); + } + } + + private static void debug(String msg) { + Thread thread = Thread.currentThread(); + Log.d(TAG, msg + " (" + thread.getName() + "-" + thread.getId() + ")"); + } } diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java index 6a02fc9..49c94d1 100644 --- a/core/java/android/app/SuggestionsAdapter.java +++ b/core/java/android/app/SuggestionsAdapter.java @@ -20,62 +20,103 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources.NotFoundException; import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; +import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Bundle; import android.server.search.SearchableInfo; import android.text.Html; import android.text.TextUtils; +import android.util.DisplayMetrics; import android.util.Log; +import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; -import android.widget.CursorAdapter; +import android.widget.AbsListView; import android.widget.ImageView; import android.widget.ResourceCursorAdapter; import android.widget.TextView; +import static android.app.SearchManager.DialogCursorProtocol; + import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; import java.util.WeakHashMap; /** * Provides the contents for the suggestion drop-down list.in {@link SearchDialog}. - * + * * @hide */ class SuggestionsAdapter extends ResourceCursorAdapter { + private static final boolean DBG = false; private static final String LOG_TAG = "SuggestionsAdapter"; - + + private SearchManager mSearchManager; + private SearchDialog mSearchDialog; private SearchableInfo mSearchable; private Context mProviderContext; private WeakHashMap<String, Drawable> mOutsideDrawablesCache; + private boolean mGlobalSearchMode; - // Cached column indexes, updated when the cursor changes. + // Cached column indexes, updated when the cursor changes. private int mFormatCol; private int mText1Col; private int mText2Col; private int mIconName1Col; private int mIconName2Col; - private int mIconBitmap1Col; - private int mIconBitmap2Col; - - public SuggestionsAdapter(Context context, SearchableInfo searchable, - WeakHashMap<String, Drawable> outsideDrawablesCache) { + private int mBackgroundColorCol; + + // This value is stored in SuggestionsAdapter by the SearchDialog to indicate whether + // a particular list item should be selected upon the next call to notifyDataSetChanged. + // This is used to indicate the index of the "More results..." list item so that when + // the data set changes after a click of "More results...", we can correctly tell the + // ListView to scroll to the right line item. It gets reset to NONE every time it + // is consumed. + private int mListItemToSelect = NONE; + static final int NONE = -1; + + // holds the maximum position that has been displayed to the user + int mMaxDisplayed = NONE; + + // holds the position that, when displayed, should result in notifying the cursor + int mDisplayNotifyPos = NONE; + + private final Runnable mStartSpinnerRunnable; + private final Runnable mStopSpinnerRunnable; + + public SuggestionsAdapter(Context context, SearchDialog searchDialog, SearchableInfo searchable, + WeakHashMap<String, Drawable> outsideDrawablesCache, boolean globalSearchMode) { super(context, com.android.internal.R.layout.search_dropdown_item_icons_2line, null, // no initial cursor true); // auto-requery + mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); + mSearchDialog = searchDialog; mSearchable = searchable; - + // set up provider resources (gives us icons, etc.) Context activityContext = mSearchable.getActivityContext(mContext); mProviderContext = mSearchable.getProviderContext(mContext, activityContext); - + mOutsideDrawablesCache = outsideDrawablesCache; + mGlobalSearchMode = globalSearchMode; + + mStartSpinnerRunnable = new Runnable() { + public void run() { + mSearchDialog.setWorking(true); + } + }; + + mStopSpinnerRunnable = new Runnable() { + public void run() { + mSearchDialog.setWorking(false); + } + }; } - + /** * Overridden to always return <code>false</code>, since we cannot be sure that * suggestion sources return stable IDs. @@ -94,20 +135,41 @@ class SuggestionsAdapter extends ResourceCursorAdapter { public Cursor runQueryOnBackgroundThread(CharSequence constraint) { if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")"); String query = (constraint == null) ? "" : constraint.toString(); + if (!mGlobalSearchMode) { + /** + * for in app search we show the progress spinner until the cursor is returned with + * the results. for global search we manage the progress bar using + * {@link DialogCursorProtocol#POST_REFRESH_RECEIVE_ISPENDING}. + */ + mSearchDialog.getWindow().getDecorView().post(mStartSpinnerRunnable); + } try { - return SearchManager.getSuggestions(mContext, mSearchable, query); + final Cursor cursor = mSearchManager.getSuggestions(mSearchable, query); + // trigger fill window so the spinner stays up until the results are copied over and + // closer to being ready + if (!mGlobalSearchMode && cursor != null) cursor.getCount(); + return cursor; } catch (RuntimeException e) { Log.w(LOG_TAG, "Search suggestions query threw an exception.", e); return null; + } finally { + if (!mGlobalSearchMode) { + mSearchDialog.getWindow().getDecorView().post(mStopSpinnerRunnable); + } } } - + /** * Cache columns. */ @Override public void changeCursor(Cursor c) { if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")"); + + if (mCursor != null) { + callCursorPreClose(mCursor); + } + super.changeCursor(c); if (c != null) { mFormatCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT); @@ -115,21 +177,86 @@ class SuggestionsAdapter extends ResourceCursorAdapter { mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2); mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1); mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2); - mIconBitmap1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1_BITMAP); - mIconBitmap2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2_BITMAP); + mBackgroundColorCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_BACKGROUND_COLOR); + } + } + + /** + * Handle sending and receiving information associated with + * {@link DialogCursorProtocol#PRE_CLOSE}. + * + * @param cursor The cursor to call. + */ + private void callCursorPreClose(Cursor cursor) { + if (!mGlobalSearchMode) return; + final Bundle request = new Bundle(); + request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.PRE_CLOSE); + request.putInt(DialogCursorProtocol.PRE_CLOSE_SEND_MAX_DISPLAY_POS, mMaxDisplayed); + final Bundle response = cursor.respond(request); + + mMaxDisplayed = -1; + } + + @Override + public void notifyDataSetChanged() { + if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged"); + super.notifyDataSetChanged(); + + callCursorPostRefresh(mCursor); + + // look out for the pending item we are supposed to scroll to + if (mListItemToSelect != NONE) { + mSearchDialog.setListSelection(mListItemToSelect); + mListItemToSelect = NONE; } } - + + /** + * Handle sending and receiving information associated with + * {@link DialogCursorProtocol#POST_REFRESH}. + * + * @param cursor The cursor to call. + */ + private void callCursorPostRefresh(Cursor cursor) { + if (!mGlobalSearchMode) return; + final Bundle request = new Bundle(); + request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.POST_REFRESH); + final Bundle response = cursor.respond(request); + + mSearchDialog.setWorking( + response.getBoolean(DialogCursorProtocol.POST_REFRESH_RECEIVE_ISPENDING, false)); + + mDisplayNotifyPos = + response.getInt(DialogCursorProtocol.POST_REFRESH_RECEIVE_DISPLAY_NOTIFY, -1); + } + + /** + * Tell the cursor which position was clicked, handling sending and receiving information + * associated with {@link DialogCursorProtocol#CLICK}. + * + * @param cursor The cursor + * @param position The position that was clicked. + */ + void callCursorOnClick(Cursor cursor, int position) { + if (!mGlobalSearchMode) return; + final Bundle request = new Bundle(1); + request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.CLICK); + request.putInt(DialogCursorProtocol.CLICK_SEND_POSITION, position); + final Bundle response = cursor.respond(request); + mListItemToSelect = response.getInt( + DialogCursorProtocol.CLICK_RECEIVE_SELECTED_POS, SuggestionsAdapter.NONE); + } + /** * Tags the view with cached child view look-ups. */ @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - View v = super.newView(context, cursor, parent); + View v = new SuggestionItemView(context, cursor); v.setTag(new ChildViewCache(v)); return v; } - + /** * Cache of the child views of drop-drown list items, to avoid looking up the children * each time the contents of a list item are changed. @@ -139,7 +266,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter { public final TextView mText2; public final ImageView mIcon1; public final ImageView mIcon2; - + public ChildViewCache(View v) { mText1 = (TextView) v.findViewById(com.android.internal.R.id.text1); mText2 = (TextView) v.findViewById(com.android.internal.R.id.text2); @@ -147,21 +274,38 @@ class SuggestionsAdapter extends ResourceCursorAdapter { mIcon2 = (ImageView) v.findViewById(com.android.internal.R.id.icon2); } } - + @Override public void bindView(View view, Context context, Cursor cursor) { ChildViewCache views = (ChildViewCache) view.getTag(); - boolean isHtml = false; - if (mFormatCol >= 0) { - String format = cursor.getString(mFormatCol); - isHtml = "html".equals(format); + final int pos = cursor.getPosition(); + + // update the maximum position displayed since last refresh + if (pos > mMaxDisplayed) { + mMaxDisplayed = pos; } + + // if the cursor wishes to be notified about this position, send it + if (mGlobalSearchMode && mDisplayNotifyPos != NONE && pos == mDisplayNotifyPos) { + final Bundle request = new Bundle(); + request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.THRESH_HIT); + mCursor.respond(request); + mDisplayNotifyPos = NONE; // only notify the first time + } + + int backgroundColor = 0; + if (mBackgroundColorCol != -1) { + backgroundColor = cursor.getInt(mBackgroundColorCol); + } + ((SuggestionItemView)view).setColor(backgroundColor); + + final boolean isHtml = mFormatCol > 0 && "html".equals(cursor.getString(mFormatCol)); setViewText(cursor, views.mText1, mText1Col, isHtml); setViewText(cursor, views.mText2, mText2Col, isHtml); - setViewIcon(cursor, views.mIcon1, mIconBitmap1Col, mIconName1Col); - setViewIcon(cursor, views.mIcon2, mIconBitmap2Col, mIconName2Col); + setViewIcon(cursor, views.mIcon1, mIconName1Col); + setViewIcon(cursor, views.mIcon2, mIconName2Col); } - + private void setViewText(Cursor cursor, TextView v, int textCol, boolean isHtml) { if (v == null) { return; @@ -173,49 +317,46 @@ class SuggestionsAdapter extends ResourceCursorAdapter { } // Set the text even if it's null, since we need to clear any previous text. v.setText(text); - + if (TextUtils.isEmpty(text)) { v.setVisibility(View.GONE); } else { v.setVisibility(View.VISIBLE); } } - - private void setViewIcon(Cursor cursor, ImageView v, int iconBitmapCol, int iconNameCol) { + + private void setViewIcon(Cursor cursor, ImageView v, int iconNameCol) { if (v == null) { return; } - Drawable drawable = null; - // First try the bitmap column - if (iconBitmapCol >= 0) { - byte[] data = cursor.getBlob(iconBitmapCol); - if (data != null) { - Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); - if (bitmap != null) { - drawable = new BitmapDrawable(bitmap); - } - } - } - // If there was no bitmap, try the icon resource column. - if (drawable == null && iconNameCol >= 0) { - String value = cursor.getString(iconNameCol); - drawable = getDrawableFromResourceValue(value); + if (iconNameCol < 0) { + return; } + String value = cursor.getString(iconNameCol); + Drawable drawable = getDrawableFromResourceValue(value); // Set the icon even if the drawable is null, since we need to clear any // previous icon. v.setImageDrawable(drawable); - + if (drawable == null) { v.setVisibility(View.GONE); } else { v.setVisibility(View.VISIBLE); + + // This is a hack to get any animated drawables (like a 'working' spinner) + // to animate. You have to setVisible true on an AnimationDrawable to get + // it to start animating, but it must first have been false or else the + // call to setVisible will be ineffective. We need to clear up the story + // about animated drawables in the future, see http://b/1878430. + drawable.setVisible(false, false); + drawable.setVisible(true, false); } } - + /** * Gets the text to show in the query field when a suggestion is selected. - * - * @param cursor The Cursor to read the suggestion data from. The Cursor should already + * + * @param cursor The Cursor to read the suggestion data from. The Cursor should already * be moved to the suggestion that is to be read from. * @return The text to show, or <code>null</code> if the query should not be * changed when selecting this suggestion. @@ -225,36 +366,36 @@ class SuggestionsAdapter extends ResourceCursorAdapter { if (cursor == null) { return null; } - + String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY); if (query != null) { return query; } - + if (mSearchable.shouldRewriteQueryFromData()) { String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA); if (data != null) { return data; } } - + if (mSearchable.shouldRewriteQueryFromText()) { String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1); if (text1 != null) { return text1; } } - + return null; } - + /** * This method is overridden purely to provide a bit of protection against * flaky content providers. - * + * * @see android.widget.ListAdapter#getView(int, View, ViewGroup) */ - @Override + @Override public View getView(int position, View convertView, ViewGroup parent) { try { return super.getView(position, convertView, parent); @@ -263,28 +404,28 @@ class SuggestionsAdapter extends ResourceCursorAdapter { // Put exception string in item title View v = newView(mContext, mCursor, parent); if (v != null) { - ChildViewCache views = (ChildViewCache) v.getTag(); + ChildViewCache views = (ChildViewCache) v.getTag(); TextView tv = views.mText1; tv.setText(e.toString()); } return v; } } - + /** * Gets a drawable given a value provided by a suggestion provider. - * + * * This value could be just the string value of a resource id * (e.g., "2130837524"), in which case we will try to retrieve a drawable from * the provider's resources. If the value is not an integer, it is - * treated as a Uri and opened with + * treated as a Uri and opened with * {@link ContentResolver#openOutputStream(android.net.Uri, String)}. * * All resources and URIs are read using the suggestion provider's context. * * If the string is not formatted as expected, or no drawable can be found for * the provided value, this method returns null. - * + * * @param drawableId a string like "2130837524", * "android.resource://com.android.alarmclock/2130837524", * or "content://contacts/photos/253". @@ -294,43 +435,58 @@ class SuggestionsAdapter extends ResourceCursorAdapter { if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) { return null; } - + // First, check the cache. Drawable drawable = mOutsideDrawablesCache.get(drawableId); - if (drawable != null) return drawable; + if (drawable != null) { + if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + drawableId); + return drawable; + } try { // Not cached, try using it as a plain resource ID in the provider's context. int resourceId = Integer.parseInt(drawableId); drawable = mProviderContext.getResources().getDrawable(resourceId); + if (DBG) Log.d(LOG_TAG, "Found icon by resource ID: " + drawableId); } catch (NumberFormatException nfe) { // The id was not an integer resource id. // Let the ContentResolver handle content, android.resource and file URIs. try { Uri uri = Uri.parse(drawableId); - drawable = Drawable.createFromStream( - mProviderContext.getContentResolver().openInputStream(uri), - null); + InputStream stream = mProviderContext.getContentResolver().openInputStream(uri); + if (stream != null) { + try { + drawable = Drawable.createFromStream(stream, null); + } finally { + try { + stream.close(); + } catch (IOException ex) { + Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex); + } + } + } + if (DBG) Log.d(LOG_TAG, "Opened icon input stream: " + drawableId); } catch (FileNotFoundException fnfe) { + if (DBG) Log.d(LOG_TAG, "Icon stream not found: " + drawableId); // drawable = null; } - + // If we got a drawable for this resource id, then stick it in the // map so we don't do this lookup again. if (drawable != null) { mOutsideDrawablesCache.put(drawableId, drawable); } } catch (NotFoundException nfe) { - // Resource could not be found + if (DBG) Log.d(LOG_TAG, "Icon resource not found: " + drawableId); // drawable = null; } - + return drawable; } - + /** * Gets the value of a string column by name. - * + * * @param cursor Cursor to read the value from. * @param columnName The name of the column to read. * @return The value of the given column, or <code>null</null> @@ -338,10 +494,75 @@ class SuggestionsAdapter extends ResourceCursorAdapter { */ public static String getColumnString(Cursor cursor, String columnName) { int col = cursor.getColumnIndex(columnName); - if (col == -1) { + if (col == NONE) { return null; } return cursor.getString(col); } + /** + * A parent viewgroup class which holds the actual suggestion item as a child. + * + * The sole purpose of this class is to draw the given background color when the item is in + * normal state and not draw the background color when it is pressed, so that when pressed the + * list view's selection highlight will be displayed properly (if we draw our background it + * draws on top of the list view selection highlight). + */ + private class SuggestionItemView extends ViewGroup { + private int mBackgroundColor; // the background color to draw in normal state. + private View mView; // the suggestion item's view. + + protected SuggestionItemView(Context context, Cursor cursor) { + // Initialize ourselves + super(context); + mBackgroundColor = 0; // transparent by default. + + // For our layout use the default list item height from the current theme. + TypedValue lineHeight = new TypedValue(); + context.getTheme().resolveAttribute( + com.android.internal.R.attr.searchResultListItemHeight, lineHeight, true); + DisplayMetrics metrics = new DisplayMetrics(); + metrics.setToDefaults(); + AbsListView.LayoutParams layout = new AbsListView.LayoutParams( + AbsListView.LayoutParams.FILL_PARENT, + (int)lineHeight.getDimension(metrics)); + + setLayoutParams(layout); + + // Initialize the child view + mView = SuggestionsAdapter.super.newView(context, cursor, this); + if (mView != null) { + addView(mView, layout.width, layout.height); + mView.setVisibility(View.VISIBLE); + } + } + + public void setColor(int backgroundColor) { + mBackgroundColor = backgroundColor; + } + + @Override + public void dispatchDraw(Canvas canvas) { + if (mBackgroundColor != 0 && !isPressed() && !isSelected()) { + canvas.drawColor(mBackgroundColor); + } + super.dispatchDraw(canvas); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mView != null) { + mView.measure(widthMeasureSpec, heightMeasureSpec); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (mView != null) { + mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight()); + } + } + } + } diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index 10c2b02..03e8623 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -26,7 +26,6 @@ import android.widget.RemoteViews; import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import com.android.internal.appwidget.IAppWidgetHost; import com.android.internal.appwidget.IAppWidgetService; @@ -40,7 +39,7 @@ public class AppWidgetHost { static final int HANDLE_UPDATE = 1; static final int HANDLE_PROVIDER_CHANGED = 2; - static Object sServiceLock = new Object(); + final static Object sServiceLock = new Object(); static IAppWidgetService sService; Context mContext; @@ -85,7 +84,7 @@ public class AppWidgetHost { int mHostId; Callbacks mCallbacks = new Callbacks(); - HashMap<Integer,AppWidgetHostView> mViews = new HashMap(); + final HashMap<Integer,AppWidgetHostView> mViews = new HashMap<Integer, AppWidgetHostView>(); public AppWidgetHost(Context context, int hostId) { mContext = context; @@ -104,8 +103,8 @@ public class AppWidgetHost { * becomes visible, i.e. from onStart() in your Activity. */ public void startListening() { - int[] updatedIds = null; - ArrayList<RemoteViews> updatedViews = new ArrayList(); + int[] updatedIds; + ArrayList<RemoteViews> updatedViews = new ArrayList<RemoteViews>(); try { if (mPackageName == null) { @@ -209,7 +208,7 @@ public class AppWidgetHost { synchronized (mViews) { mViews.put(appWidgetId, view); } - RemoteViews views = null; + RemoteViews views; try { views = sService.getAppWidgetViews(appWidgetId); } catch (RemoteException e) { @@ -231,6 +230,7 @@ public class AppWidgetHost { /** * Called when the AppWidget provider for a AppWidget has been upgraded to a new apk. */ + @SuppressWarnings({"UnusedDeclaration"}) protected void onProviderChanged(int appWidgetId, AppWidgetProviderInfo appWidget) { } diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index be0f96e..62d9267 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -22,16 +22,12 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; -import android.os.Handler; -import android.os.Message; import android.os.SystemClock; -import android.util.Config; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.animation.Animation; import android.widget.FrameLayout; import android.widget.RemoteViews; import android.widget.TextView; @@ -86,6 +82,7 @@ public class AppWidgetHostView extends FrameLayout { * @param animationIn Resource ID of in animation to use * @param animationOut Resource ID of out animation to use */ + @SuppressWarnings({"UnusedDeclaration"}) public AppWidgetHostView(Context context, int animationIn, int animationOut) { super(context); mContext = context; @@ -272,7 +269,7 @@ public class AppWidgetHostView extends FrameLayout { try { if (mInfo != null) { Context theirContext = mContext.createPackageContext( - mInfo.provider.getPackageName(), 0 /* no flags */); + mInfo.provider.getPackageName(), Context.CONTEXT_RESTRICTED); LayoutInflater inflater = (LayoutInflater) theirContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater = inflater.cloneInContext(theirContext); diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index eca04b3..3660001 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -21,7 +21,9 @@ import android.content.Context; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; +import android.util.DisplayMetrics; import android.util.Log; +import android.util.TypedValue; import android.widget.RemoteViews; import com.android.internal.appwidget.IAppWidgetService; @@ -187,6 +189,8 @@ public class AppWidgetManager { Context mContext; + private DisplayMetrics mDisplayMetrics; + /** * Get the AppWidgetManager instance to use for the supplied {@link android.content.Context * Context} object. @@ -213,6 +217,7 @@ public class AppWidgetManager { private AppWidgetManager(Context context) { mContext = context; + mDisplayMetrics = context.getResources().getDisplayMetrics(); } /** @@ -292,7 +297,15 @@ public class AppWidgetManager { */ public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) { try { - return sService.getAppWidgetInfo(appWidgetId); + AppWidgetProviderInfo info = sService.getAppWidgetInfo(appWidgetId); + if (info != null) { + // Converting complex to dp. + info.minWidth = + TypedValue.complexToDimensionPixelSize(info.minWidth, mDisplayMetrics); + info.minHeight = + TypedValue.complexToDimensionPixelSize(info.minHeight, mDisplayMetrics); + } + return info; } catch (RemoteException e) { throw new RuntimeException("system server dead?", e); diff --git a/core/java/android/backup/AbsoluteFileBackupHelper.java b/core/java/android/backup/AbsoluteFileBackupHelper.java new file mode 100644 index 0000000..ab24675 --- /dev/null +++ b/core/java/android/backup/AbsoluteFileBackupHelper.java @@ -0,0 +1,66 @@ +/* + * 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.backup; + +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.File; +import java.io.FileDescriptor; + +/** + * Like FileBackupHelper, but takes absolute paths for the files instead of + * subpaths of getFilesDir() + * + * @hide + */ +public class AbsoluteFileBackupHelper extends FileBackupHelperBase implements BackupHelper { + private static final String TAG = "AbsoluteFileBackupHelper"; + + Context mContext; + String[] mFiles; + + public AbsoluteFileBackupHelper(Context context, String... files) { + super(context); + + mContext = context; + mFiles = files; + } + + /** + * Based on oldState, determine which of the files from the application's data directory + * need to be backed up, write them to the data stream, and fill in newState with the + * state as it exists now. + */ + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) { + // use the file paths as the keys, too + performBackup_checked(oldState, data, newState, mFiles, mFiles); + } + + public void restoreEntity(BackupDataInputStream data) { + // TODO: turn this off before ship + Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size()); + String key = data.getKey(); + if (isKeyInList(key, mFiles)) { + File f = new File(key); + writeFile(f, data); + } + } +} + diff --git a/core/java/android/backup/BackupDataInput.java b/core/java/android/backup/BackupDataInput.java new file mode 100644 index 0000000..69c206c --- /dev/null +++ b/core/java/android/backup/BackupDataInput.java @@ -0,0 +1,117 @@ +/* + * 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.backup; + +import android.content.Context; + +import java.io.FileDescriptor; +import java.io.IOException; + +/** @hide */ +public class BackupDataInput { + int mBackupReader; + + private EntityHeader mHeader = new EntityHeader(); + private boolean mHeaderReady; + + private static class EntityHeader { + String key; + int dataSize; + } + + public BackupDataInput(FileDescriptor fd) { + if (fd == null) throw new NullPointerException(); + mBackupReader = ctor(fd); + if (mBackupReader == 0) { + throw new RuntimeException("Native initialization failed with fd=" + fd); + } + } + + protected void finalize() throws Throwable { + try { + dtor(mBackupReader); + } finally { + super.finalize(); + } + } + + public boolean readNextHeader() throws IOException { + int result = readNextHeader_native(mBackupReader, mHeader); + if (result == 0) { + // read successfully + mHeaderReady = true; + return true; + } else if (result > 0) { + // done + mHeaderReady = false; + return false; + } else { + // error + mHeaderReady = false; + throw new IOException("result=0x" + Integer.toHexString(result)); + } + } + + public String getKey() { + if (mHeaderReady) { + return mHeader.key; + } else { + throw new IllegalStateException("mHeaderReady=false"); + } + } + + public int getDataSize() { + if (mHeaderReady) { + return mHeader.dataSize; + } else { + throw new IllegalStateException("mHeaderReady=false"); + } + } + + public int readEntityData(byte[] data, int offset, int size) throws IOException { + if (mHeaderReady) { + int result = readEntityData_native(mBackupReader, data, offset, size); + if (result >= 0) { + return result; + } else { + throw new IOException("result=0x" + Integer.toHexString(result)); + } + } else { + throw new IllegalStateException("mHeaderReady=false"); + } + } + + public void skipEntityData() throws IOException { + if (mHeaderReady) { + int result = skipEntityData_native(mBackupReader); + if (result >= 0) { + return; + } else { + throw new IOException("result=0x" + Integer.toHexString(result)); + } + } else { + throw new IllegalStateException("mHeaderReady=false"); + } + } + + private native static int ctor(FileDescriptor fd); + private native static void dtor(int mBackupReader); + + private native int readNextHeader_native(int mBackupReader, EntityHeader entity); + private native int readEntityData_native(int mBackupReader, byte[] data, int offset, int size); + private native int skipEntityData_native(int mBackupReader); +} diff --git a/core/java/android/backup/BackupDataInputStream.java b/core/java/android/backup/BackupDataInputStream.java new file mode 100644 index 0000000..b705c4c --- /dev/null +++ b/core/java/android/backup/BackupDataInputStream.java @@ -0,0 +1,63 @@ +/* + * 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.backup; + +import android.util.Log; + +import java.io.InputStream; +import java.io.IOException; + +/** @hide */ +public class BackupDataInputStream extends InputStream { + + String key; + int dataSize; + + BackupDataInput mData; + byte[] mOneByte; + + BackupDataInputStream(BackupDataInput data) { + mData = data; + } + + public int read() throws IOException { + byte[] one = mOneByte; + if (mOneByte == null) { + one = mOneByte = new byte[1]; + } + mData.readEntityData(one, 0, 1); + return one[0]; + } + + public int read(byte[] b, int offset, int size) throws IOException { + return mData.readEntityData(b, offset, size); + } + + public int read(byte[] b) throws IOException { + return mData.readEntityData(b, 0, b.length); + } + + public String getKey() { + return this.key; + } + + public int size() { + return this.dataSize; + } +} + + diff --git a/core/java/android/backup/BackupDataOutput.java b/core/java/android/backup/BackupDataOutput.java index 555494e..d29c5ba 100644 --- a/core/java/android/backup/BackupDataOutput.java +++ b/core/java/android/backup/BackupDataOutput.java @@ -19,27 +19,59 @@ package android.backup; import android.content.Context; import java.io.FileDescriptor; +import java.io.IOException; /** @hide */ public class BackupDataOutput { - /* package */ FileDescriptor fd; + int mBackupWriter; public static final int OP_UPDATE = 1; public static final int OP_DELETE = 2; - public BackupDataOutput(Context context, FileDescriptor fd) { - this.fd = fd; + public BackupDataOutput(FileDescriptor fd) { + if (fd == null) throw new NullPointerException(); + mBackupWriter = ctor(fd); + if (mBackupWriter == 0) { + throw new RuntimeException("Native initialization failed with fd=" + fd); + } } - public void close() { - // do we close the fd? + // A dataSize of -1 indicates that the record under this key should be deleted + public int writeEntityHeader(String key, int dataSize) throws IOException { + int result = writeEntityHeader_native(mBackupWriter, key, dataSize); + if (result >= 0) { + return result; + } else { + throw new IOException("result=0x" + Integer.toHexString(result)); + } } - public native void flush(); - public native void write(byte[] buffer); - public native void write(int oneByte); - public native void write(byte[] buffer, int offset, int count); - public native void writeOperation(int op); - public native void writeKey(String key); + public int writeEntityData(byte[] data, int size) throws IOException { + int result = writeEntityData_native(mBackupWriter, data, size); + if (result >= 0) { + return result; + } else { + throw new IOException("result=0x" + Integer.toHexString(result)); + } + } + + public void setKeyPrefix(String keyPrefix) { + setKeyPrefix_native(mBackupWriter, keyPrefix); + } + + protected void finalize() throws Throwable { + try { + dtor(mBackupWriter); + } finally { + super.finalize(); + } + } + + private native static int ctor(FileDescriptor fd); + private native static void dtor(int mBackupWriter); + + private native static int writeEntityHeader_native(int mBackupWriter, String key, int dataSize); + private native static int writeEntityData_native(int mBackupWriter, byte[] data, int size); + private native static void setKeyPrefix_native(int mBackupWriter, String keyPrefix); } diff --git a/core/java/android/backup/BackupHelper.java b/core/java/android/backup/BackupHelper.java new file mode 100644 index 0000000..3983e28 --- /dev/null +++ b/core/java/android/backup/BackupHelper.java @@ -0,0 +1,46 @@ +/* + * 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.backup; + +import android.os.ParcelFileDescriptor; + +import java.io.InputStream; + +/** @hide */ +public interface BackupHelper { + /** + * Based on oldState, determine which of the files from the application's data directory + * need to be backed up, write them to the data stream, and fill in newState with the + * state as it exists now. + */ + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState); + + /** + * Called by BackupHelperDispatcher to dispatch one entity of data. + * <p class=note> + * Do not close the <code>data</code> stream. Do not read more than + * <code>dataSize</code> bytes from <code>data</code>. + */ + public void restoreEntity(BackupDataInputStream data); + + /** + * + */ + public void writeRestoreSnapshot(ParcelFileDescriptor fd); +} + diff --git a/core/java/android/backup/BackupHelperAgent.java b/core/java/android/backup/BackupHelperAgent.java new file mode 100644 index 0000000..5d0c4a2 --- /dev/null +++ b/core/java/android/backup/BackupHelperAgent.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.backup; + +import android.app.BackupAgent; +import android.backup.BackupHelper; +import android.backup.BackupHelperDispatcher; +import android.backup.BackupDataInput; +import android.backup.BackupDataOutput; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.IOException; + +/** @hide */ +public class BackupHelperAgent extends BackupAgent { + static final String TAG = "BackupHelperAgent"; + + BackupHelperDispatcher mDispatcher = new BackupHelperDispatcher(); + + @Override + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) throws IOException { + mDispatcher.performBackup(oldState, data, newState); + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) + throws IOException { + mDispatcher.performRestore(data, appVersionCode, newState); + } + + public BackupHelperDispatcher getDispatcher() { + return mDispatcher; + } + + public void addHelper(String keyPrefix, BackupHelper helper) { + mDispatcher.addHelper(keyPrefix, helper); + } +} + + diff --git a/core/java/android/backup/BackupHelperDispatcher.java b/core/java/android/backup/BackupHelperDispatcher.java new file mode 100644 index 0000000..6ccb83e --- /dev/null +++ b/core/java/android/backup/BackupHelperDispatcher.java @@ -0,0 +1,151 @@ +/* + * 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.backup; + +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.FileDescriptor; +import java.util.TreeMap; +import java.util.Map; + +/** @hide */ +public class BackupHelperDispatcher { + private static final String TAG = "BackupHelperDispatcher"; + + private static class Header { + int chunkSize; // not including the header + String keyPrefix; + } + + TreeMap<String,BackupHelper> mHelpers = new TreeMap<String,BackupHelper>(); + + public BackupHelperDispatcher() { + } + + public void addHelper(String keyPrefix, BackupHelper helper) { + mHelpers.put(keyPrefix, helper); + } + + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) throws IOException { + // First, do the helpers that we've already done, since they're already in the state + // file. + int err; + Header header = new Header(); + TreeMap<String,BackupHelper> helpers = (TreeMap<String,BackupHelper>)mHelpers.clone(); + FileDescriptor oldStateFD = null; + FileDescriptor newStateFD = newState.getFileDescriptor(); + + if (oldState != null) { + oldStateFD = oldState.getFileDescriptor(); + while ((err = readHeader_native(header, oldStateFD)) >= 0) { + if (err == 0) { + BackupHelper helper = helpers.get(header.keyPrefix); + Log.d(TAG, "handling existing helper '" + header.keyPrefix + "' " + helper); + if (helper != null) { + doOneBackup(oldState, data, newState, header, helper); + helpers.remove(header.keyPrefix); + } else { + skipChunk_native(oldStateFD, header.chunkSize); + } + } + } + } + + // Then go through and do the rest that we haven't done. + for (Map.Entry<String,BackupHelper> entry: helpers.entrySet()) { + header.keyPrefix = entry.getKey(); + Log.d(TAG, "handling new helper '" + header.keyPrefix + "'"); + BackupHelper helper = entry.getValue(); + doOneBackup(oldState, data, newState, header, helper); + } + } + + private void doOneBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState, Header header, BackupHelper helper) + throws IOException { + int err; + FileDescriptor newStateFD = newState.getFileDescriptor(); + + // allocate space for the header in the file + int pos = allocateHeader_native(header, newStateFD); + if (pos < 0) { + throw new IOException("allocateHeader_native failed (error " + pos + ")"); + } + + data.setKeyPrefix(header.keyPrefix); + + // do the backup + helper.performBackup(oldState, data, newState); + + // fill in the header (seeking back to pos). The file pointer will be returned to + // where it was at the end of performBackup. Header.chunkSize will not be filled in. + err = writeHeader_native(header, newStateFD, pos); + if (err != 0) { + throw new IOException("writeHeader_native failed (error " + err + ")"); + } + } + + public void performRestore(BackupDataInput input, int appVersionCode, + ParcelFileDescriptor newState) + throws IOException { + boolean alreadyComplained = false; + + BackupDataInputStream stream = new BackupDataInputStream(input); + while (input.readNextHeader()) { + + String rawKey = input.getKey(); + int pos = rawKey.indexOf(':'); + if (pos > 0) { + String prefix = rawKey.substring(0, pos); + BackupHelper helper = mHelpers.get(prefix); + if (helper != null) { + stream.dataSize = input.getDataSize(); + stream.key = rawKey.substring(pos+1); + helper.restoreEntity(stream); + } else { + if (!alreadyComplained) { + Log.w(TAG, "Couldn't find helper for: '" + rawKey + "'"); + alreadyComplained = true; + } + } + } else { + if (!alreadyComplained) { + Log.w(TAG, "Entity with no prefix: '" + rawKey + "'"); + alreadyComplained = true; + } + } + input.skipEntityData(); // In case they didn't consume the data. + } + + // Write out the state files -- mHelpers is a TreeMap, so the order is well defined. + for (BackupHelper helper: mHelpers.values()) { + helper.writeRestoreSnapshot(newState); + } + } + + private static native int readHeader_native(Header h, FileDescriptor fd); + private static native int skipChunk_native(FileDescriptor fd, int bytesToSkip); + + private static native int allocateHeader_native(Header h, FileDescriptor fd); + private static native int writeHeader_native(Header h, FileDescriptor fd, int pos); +} + diff --git a/core/java/android/backup/BackupManager.java b/core/java/android/backup/BackupManager.java index 6f0b2ee..34a1a0c 100644 --- a/core/java/android/backup/BackupManager.java +++ b/core/java/android/backup/BackupManager.java @@ -19,6 +19,7 @@ package android.backup; import android.content.Context; import android.os.RemoteException; import android.os.ServiceManager; +import android.util.Log; /** * BackupManager is the interface to the system's backup service. @@ -32,14 +33,24 @@ import android.os.ServiceManager; * until the backup operation actually occurs. * * <p>The backup operation itself begins with the system launching the - * {@link BackupService} subclass declared in your manifest. See the documentation - * for {@link BackupService} for a detailed description of how the backup then proceeds. + * {@link android.app.BackupAgent} subclass declared in your manifest. See the + * documentation for {@link android.app.BackupAgent} for a detailed description + * of how the backup then proceeds. * * @hide pending API solidification */ public class BackupManager { + private static final String TAG = "BackupManager"; + private Context mContext; - private IBackupManager mService; + private static IBackupManager sService; + + private static void checkServiceBinder() { + if (sService == null) { + sService = IBackupManager.Stub.asInterface( + ServiceManager.getService(Context.BACKUP_SERVICE)); + } + } /** * Constructs a BackupManager object through which the application can @@ -51,19 +62,60 @@ public class BackupManager { */ public BackupManager(Context context) { mContext = context; - mService = IBackupManager.Stub.asInterface( - ServiceManager.getService(Context.BACKUP_SERVICE)); } /** * Notifies the Android backup system that your application wishes to back up * new changes to its data. A backup operation using your application's - * {@link BackupService} subclass will be scheduled when you call this method. + * {@link android.app.BackupAgent} subclass will be scheduled when you call this method. */ public void dataChanged() { - try { - mService.dataChanged(mContext.getPackageName()); - } catch (RemoteException e) { + checkServiceBinder(); + if (sService != null) { + try { + sService.dataChanged(mContext.getPackageName()); + } catch (RemoteException e) { + Log.d(TAG, "dataChanged() couldn't connect"); + } + } + } + + /** + * Convenience method for callers who need to indicate that some other package + * needs a backup pass. This can be relevant in the case of groups of packages + * that share a uid, for example. + * + * This method requires that the application hold the "android.permission.BACKUP" + * permission if the package named in the argument is not the caller's own. + */ + public static void dataChanged(String packageName) { + checkServiceBinder(); + if (sService != null) { + try { + sService.dataChanged(packageName); + } catch (RemoteException e) { + Log.d(TAG, "dataChanged(pkg) couldn't connect"); + } + } + } + + /** + * Begin the process of restoring system data from backup. This method requires + * that the application hold the "android.permission.BACKUP" permission, and is + * not public. + * + * {@hide} + */ + public IRestoreSession beginRestoreSession(String transport) { + IRestoreSession binder = null; + checkServiceBinder(); + if (sService != null) { + try { + binder = sService.beginRestoreSession(transport); + } catch (RemoteException e) { + Log.d(TAG, "beginRestoreSession() couldn't connect"); + } } + return binder; } } diff --git a/core/java/android/backup/FileBackupHelper.java b/core/java/android/backup/FileBackupHelper.java index 05159dc..4058497 100644 --- a/core/java/android/backup/FileBackupHelper.java +++ b/core/java/android/backup/FileBackupHelper.java @@ -20,54 +20,53 @@ import android.content.Context; import android.os.ParcelFileDescriptor; import android.util.Log; +import java.io.File; import java.io.FileDescriptor; /** @hide */ -public class FileBackupHelper { +public class FileBackupHelper extends FileBackupHelperBase implements BackupHelper { private static final String TAG = "FileBackupHelper"; + Context mContext; + File mFilesDir; + String[] mFiles; + + public FileBackupHelper(Context context, String... files) { + super(context); + + mContext = context; + mFilesDir = context.getFilesDir(); + mFiles = files; + } + /** * Based on oldState, determine which of the files from the application's data directory * need to be backed up, write them to the data stream, and fill in newState with the * state as it exists now. */ - public static void performBackup(Context context, - ParcelFileDescriptor oldState, BackupDataOutput data, - ParcelFileDescriptor newState, String[] files) { - String basePath = context.getFilesDir().getAbsolutePath(); - performBackup_checked(basePath, oldState, data, newState, files); - } - - /** - * Check the parameters so the native code doens't have to throw all the exceptions - * since it's easier to do that from java. - */ - static void performBackup_checked(String basePath, - ParcelFileDescriptor oldState, BackupDataOutput data, - ParcelFileDescriptor newState, String[] files) { - if (files.length == 0) { - return; - } - if (basePath == null) { - throw new NullPointerException(); - } - // oldStateFd can be null - FileDescriptor oldStateFd = oldState != null ? oldState.getFileDescriptor() : null; - if (data.fd == null) { - throw new NullPointerException(); - } - FileDescriptor newStateFd = newState.getFileDescriptor(); - if (newStateFd == null) { - throw new NullPointerException(); + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) { + // file names + String[] files = mFiles; + File base = mContext.getFilesDir(); + final int N = files.length; + String[] fullPaths = new String[N]; + for (int i=0; i<N; i++) { + fullPaths[i] = (new File(base, files[i])).getAbsolutePath(); } - int err = performBackup_native(basePath, oldStateFd, data.fd, newStateFd, files); + // go + performBackup_checked(oldState, data, newState, fullPaths, files); + } - if (err != 0) { - throw new RuntimeException("Backup failed"); // TODO: more here + public void restoreEntity(BackupDataInputStream data) { + // TODO: turn this off before ship + Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size()); + String key = data.getKey(); + if (isKeyInList(key, mFiles)) { + File f = new File(mFilesDir, key); + writeFile(f, data); } } - - native private static int performBackup_native(String basePath, FileDescriptor oldState, - FileDescriptor data, FileDescriptor newState, String[] files); } + diff --git a/core/java/android/backup/FileBackupHelperBase.java b/core/java/android/backup/FileBackupHelperBase.java new file mode 100644 index 0000000..03ae476 --- /dev/null +++ b/core/java/android/backup/FileBackupHelperBase.java @@ -0,0 +1,130 @@ +/* + * 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.backup; + +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.InputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; + +class FileBackupHelperBase { + private static final String TAG = "RestoreHelperBase"; + + int mPtr; + Context mContext; + boolean mExceptionLogged; + + FileBackupHelperBase(Context context) { + mPtr = ctor(); + mContext = context; + } + + protected void finalize() throws Throwable { + try { + dtor(mPtr); + } finally { + super.finalize(); + } + } + + /** + * Check the parameters so the native code doens't have to throw all the exceptions + * since it's easier to do that from java. + */ + static void performBackup_checked(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState, String[] files, String[] keys) { + if (files.length == 0) { + return; + } + // files must be all absolute paths + for (String f: files) { + if (f.charAt(0) != '/') { + throw new RuntimeException("files must have all absolute paths: " + f); + } + } + // the length of files and keys must be the same + if (files.length != keys.length) { + throw new RuntimeException("files.length=" + files.length + + " keys.length=" + keys.length); + } + // oldStateFd can be null + FileDescriptor oldStateFd = oldState != null ? oldState.getFileDescriptor() : null; + FileDescriptor newStateFd = newState.getFileDescriptor(); + if (newStateFd == null) { + throw new NullPointerException(); + } + + int err = performBackup_native(oldStateFd, data.mBackupWriter, newStateFd, files, keys); + + if (err != 0) { + // TODO: more here + throw new RuntimeException("Backup failed 0x" + Integer.toHexString(err)); + } + } + + void writeFile(File f, InputStream in) { + if (!(in instanceof BackupDataInputStream)) { + throw new IllegalStateException("input stream must be a BackupDataInputStream"); + } + int result = -1; + + // Create the enclosing directory. + File parent = f.getParentFile(); + parent.mkdirs(); + + result = writeFile_native(mPtr, f.getAbsolutePath(), + ((BackupDataInputStream)in).mData.mBackupReader); + if (result != 0) { + // Bail on this entity. Only log one failure per helper object. + if (!mExceptionLogged) { + Log.e(TAG, "Failed restoring file '" + f + "' for app '" + + mContext.getPackageName() + "\' result=0x" + + Integer.toHexString(result)); + mExceptionLogged = true; + } + } + } + + public void writeRestoreSnapshot(ParcelFileDescriptor fd) { + int result = writeSnapshot_native(mPtr, fd.getFileDescriptor()); + // TODO: Do something with the error. + } + + boolean isKeyInList(String key, String[] list) { + for (String s: list) { + if (s.equals(key)) { + return true; + } + } + return false; + } + + private static native int ctor(); + private static native void dtor(int ptr); + + native private static int performBackup_native(FileDescriptor oldState, + int data, FileDescriptor newState, String[] files, String[] keys); + + private static native int writeFile_native(int ptr, String filename, int backupReader); + private static native int writeSnapshot_native(int ptr, FileDescriptor fd); +} + + diff --git a/core/java/android/backup/IBackupManager.aidl b/core/java/android/backup/IBackupManager.aidl index cf22798..9d181be 100644 --- a/core/java/android/backup/IBackupManager.aidl +++ b/core/java/android/backup/IBackupManager.aidl @@ -16,6 +16,8 @@ package android.backup; +import android.backup.IRestoreSession; + /** * Direct interface to the Backup Manager Service that applications invoke on. The only * operation currently needed is a simple notification that the app has made changes to @@ -30,12 +32,102 @@ interface IBackupManager { /** * Tell the system service that the caller has made changes to its * data, and therefore needs to undergo an incremental backup pass. + * + * Any application can invoke this method for its own package, but + * only callers who hold the android.permission.BACKUP permission + * may invoke it for arbitrary packages. + */ + void dataChanged(String packageName); + + /** + * Erase all backed-up data for the given package from the storage + * destination. + * + * Any application can invoke this method for its own package, but + * only callers who hold the android.permission.BACKUP permission + * may invoke it for arbitrary packages. + */ + void clearBackupData(String packageName); + + /** + * Notifies the Backup Manager Service that an agent has become available. This + * method is only invoked by the Activity Manager. + */ + void agentConnected(String packageName, IBinder agent); + + /** + * Notify the Backup Manager Service that an agent has unexpectedly gone away. + * This method is only invoked by the Activity Manager. + */ + void agentDisconnected(String packageName); + + /** + * Enable/disable the backup service entirely. When disabled, no backup + * or restore operations will take place. Data-changed notifications will + * still be observed and collected, however, so that changes made while the + * mechanism was disabled will still be backed up properly if it is enabled + * at some point in the future. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + */ + void setBackupEnabled(boolean isEnabled); + + /** + * Indicate that any necessary one-time provisioning has occurred. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + */ + void setBackupProvisioned(boolean isProvisioned); + + /** + * Report whether the backup mechanism is currently enabled. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + */ + boolean isBackupEnabled(); + + /** + * Schedule an immediate backup attempt for all pending updates. This is + * primarily intended for transports to use when they detect a suitable + * opportunity for doing a backup pass. If there are no pending updates to + * be sent, no action will be taken. Even if some updates are pending, the + * transport will still be asked to confirm via the usual requestBackupTime() + * method. + * + * <p>Callers must hold the android.permission.BACKUP permission to use this method. + */ + void backupNow(); + + /** + * Identify the currently selected transport. Callers must hold the + * android.permission.BACKUP permission to use this method. + */ + String getCurrentTransport(); + + /** + * Request a list of all available backup transports' names. Callers must + * hold the android.permission.BACKUP permission to use this method. + */ + String[] listAllTransports(); + + /** + * Specify the current backup transport. Callers must hold the + * android.permission.BACKUP permission to use this method. + * + * @param transport The name of the transport to select. This should be one + * of {@link BackupManager.TRANSPORT_GOOGLE} or {@link BackupManager.TRANSPORT_ADB}. + * @return The name of the previously selected transport. If the given transport + * name is not one of the currently available transports, no change is made to + * the current transport setting and the method returns null. */ - oneway void dataChanged(String packageName); + String selectBackupTransport(String transport); /** - * Schedule a full backup of the given package. - * !!! TODO: protect with a signature-or-system permission? + * Begin a restore session with the given transport (which may differ from the + * currently-active backup transport). + * + * @param transport The name of the transport to use for the restore operation. + * @return An interface to the restore session, or null on error. */ - oneway void scheduleFullBackup(String packageName); + IRestoreSession beginRestoreSession(String transportID); } diff --git a/core/java/android/backup/IRestoreObserver.aidl b/core/java/android/backup/IRestoreObserver.aidl new file mode 100644 index 0000000..59e59fc --- /dev/null +++ b/core/java/android/backup/IRestoreObserver.aidl @@ -0,0 +1,50 @@ +/* + * 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.backup; + +/** + * Callback class for receiving progress reports during a restore operation. + * + * @hide + */ +interface IRestoreObserver { + /** + * The restore operation has begun. + * + * @param numPackages The total number of packages being processed in + * this restore operation. + */ + void restoreStarting(int numPackages); + + /** + * An indication of which package is being restored currently, out of the + * total number provided in the restoreStarting() callback. This method + * is not guaranteed to be called. + * + * @param nowBeingRestored The index, between 1 and the numPackages parameter + * to the restoreStarting() callback, of the package now being restored. + */ + void onUpdate(int nowBeingRestored); + + /** + * The restore operation has completed. + * + * @param error Zero on success; a nonzero error code if the restore operation + * as a whole failed. + */ + void restoreFinished(int error); +} diff --git a/core/java/android/backup/IRestoreSession.aidl b/core/java/android/backup/IRestoreSession.aidl new file mode 100644 index 0000000..2a1fbc1 --- /dev/null +++ b/core/java/android/backup/IRestoreSession.aidl @@ -0,0 +1,55 @@ +/* + * 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.backup; + +import android.backup.RestoreSet; +import android.backup.IRestoreObserver; + +/** + * Binder interface used by clients who wish to manage a restore operation. Every + * method in this interface requires the android.permission.BACKUP permission. + * + * {@hide} + */ +interface IRestoreSession { + /** + * Ask the current transport what the available restore sets are. + * + * @return A bundle containing two elements: an int array under the key + * "tokens" whose entries are a transport-private identifier for each backup set; + * and a String array under the key "names" whose entries are the user-meaningful + * text corresponding to the backup sets at each index in the tokens array. + */ + RestoreSet[] getAvailableRestoreSets(); + + /** + * Restore the given set onto the device, replacing the current data of any app + * contained in the restore set with the data previously backed up. + * + * @param token The token from {@link getAvailableRestoreSets()} corresponding to + * the restore set that should be used. + * @param observer If non-null, this binder points to an object that will receive + * progress callbacks during the restore operation. + */ + int performRestore(long token, IRestoreObserver observer); + + /** + * End this restore session. After this method is called, the IRestoreSession binder + * is no longer valid. + */ + void endRestoreSession(); +} diff --git a/core/java/android/backup/RestoreSet.aidl b/core/java/android/backup/RestoreSet.aidl new file mode 100644 index 0000000..42e77bf --- /dev/null +++ b/core/java/android/backup/RestoreSet.aidl @@ -0,0 +1,19 @@ +/* + * 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.backup; + +parcelable RestoreSet;
\ No newline at end of file diff --git a/core/java/android/backup/RestoreSet.java b/core/java/android/backup/RestoreSet.java new file mode 100644 index 0000000..eeca148 --- /dev/null +++ b/core/java/android/backup/RestoreSet.java @@ -0,0 +1,87 @@ +/* + * 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.backup; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Descriptive information about a set of backed-up app data available for restore. + * Used by IRestoreSession clients. + * + * @hide + */ +public class RestoreSet implements Parcelable { + /** + * Name of this restore set. May be user generated, may simply be the name + * of the handset model, e.g. "T-Mobile G1". + */ + public String name; + + /** + * Identifier of the device whose data this is. This will be as unique as + * is practically possible; for example, it might be an IMEI. + */ + public String device; + + /** + * Token that identifies this backup set unambiguously to the backup/restore + * transport. This is guaranteed to be valid for the duration of a restore + * session, but is meaningless once the session has ended. + */ + public long token; + + + public RestoreSet() { + // Leave everything zero / null + } + + public RestoreSet(String _name, String _dev, long _token) { + name = _name; + device = _dev; + token = _token; + } + + + // Parcelable implementation + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeString(name); + out.writeString(device); + out.writeLong(token); + } + + public static final Parcelable.Creator<RestoreSet> CREATOR + = new Parcelable.Creator<RestoreSet>() { + public RestoreSet createFromParcel(Parcel in) { + return new RestoreSet(in); + } + + public RestoreSet[] newArray(int size) { + return new RestoreSet[size]; + } + }; + + private RestoreSet(Parcel in) { + name = in.readString(); + device = in.readString(); + token = in.readLong(); + } +} diff --git a/core/java/android/backup/SharedPreferencesBackupHelper.java b/core/java/android/backup/SharedPreferencesBackupHelper.java index 8627f08..4a7b399 100644 --- a/core/java/android/backup/SharedPreferencesBackupHelper.java +++ b/core/java/android/backup/SharedPreferencesBackupHelper.java @@ -18,24 +18,51 @@ package android.backup; import android.content.Context; import android.os.ParcelFileDescriptor; +import android.util.Log; +import java.io.File; import java.io.FileDescriptor; /** @hide */ -public class SharedPreferencesBackupHelper { - public static void performBackup(Context context, - ParcelFileDescriptor oldSnapshot, ParcelFileDescriptor newSnapshot, - BackupDataOutput data, String[] prefGroups) { - String basePath = "/xxx"; //context.getPreferencesDir(); +public class SharedPreferencesBackupHelper extends FileBackupHelperBase implements BackupHelper { + private static final String TAG = "SharedPreferencesBackupHelper"; + private Context mContext; + private String[] mPrefGroups; + + public SharedPreferencesBackupHelper(Context context, String... prefGroups) { + super(context); + + mContext = context; + mPrefGroups = prefGroups; + } + + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) { + Context context = mContext; + // make filenames for the prefGroups + String[] prefGroups = mPrefGroups; final int N = prefGroups.length; String[] files = new String[N]; for (int i=0; i<N; i++) { - files[i] = prefGroups[i] + ".xml"; + files[i] = context.getSharedPrefsFile(prefGroups[i]).getAbsolutePath(); } - FileBackupHelper.performBackup_checked(basePath, oldSnapshot, data, newSnapshot, files); + // go + performBackup_checked(oldState, data, newState, files, prefGroups); + } + + public void restoreEntity(BackupDataInputStream data) { + Context context = mContext; + + // TODO: turn this off before ship + Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size()); + String key = data.getKey(); + if (isKeyInList(key, mPrefGroups)) { + File f = context.getSharedPrefsFile(key).getAbsoluteFile(); + writeFile(f, data); + } } } diff --git a/core/java/android/bluetooth/BluetoothHeadset.java b/core/java/android/bluetooth/BluetoothHeadset.java index e198435..fe1e09a 100644 --- a/core/java/android/bluetooth/BluetoothHeadset.java +++ b/core/java/android/bluetooth/BluetoothHeadset.java @@ -332,6 +332,31 @@ public class BluetoothHeadset { } /** + * Get battery usage hint for Bluetooth Headset service. + * This is a monotonically increasing integer. Wraps to 0 at + * Integer.MAX_INT, and at boot. + * Current implementation returns the number of AT commands handled since + * boot. This is a good indicator for spammy headset/handsfree units that + * can keep the device awake by polling for cellular status updates. As a + * rule of thumb, each AT command prevents the CPU from sleeping for 500 ms + * @return monotonically increasing battery usage hint, or a negative error + * code on error + * @hide + */ + public int getBatteryUsageHint() { + if (DBG) log("getBatteryUsageHint()"); + if (mService != null) { + try { + return mService.getBatteryUsageHint(); + } catch (RemoteException e) {Log.e(TAG, e.toString());} + } else { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable())); + } + return -1; + } + + /** * Check class bits for possible HSP or HFP support. * This is a simple heuristic that tries to guess if a device with the * given class bits might support HSP or HFP. It is not accurate for all diff --git a/core/java/android/bluetooth/HeadsetBase.java b/core/java/android/bluetooth/HeadsetBase.java index f31e7a2..f987ffd 100644 --- a/core/java/android/bluetooth/HeadsetBase.java +++ b/core/java/android/bluetooth/HeadsetBase.java @@ -40,6 +40,8 @@ public class HeadsetBase { public static final int DIRECTION_INCOMING = 1; public static final int DIRECTION_OUTGOING = 2; + private static int sAtInputCount = 0; /* TODO: Consider not using a static variable */ + private final BluetoothDevice mBluetooth; private final String mAddress; private final int mRfcommChannel; @@ -109,6 +111,14 @@ public class HeadsetBase { acquireWakeLock(); long timestamp; + synchronized(HeadsetBase.class) { + if (sAtInputCount == Integer.MAX_VALUE) { + sAtInputCount = 0; + } else { + sAtInputCount++; + } + } + if (DBG) timestamp = System.currentTimeMillis(); AtCommandResult result = mAtParser.process(input); if (DBG) Log.d(TAG, "Processing " + input + " took " + @@ -279,7 +289,11 @@ public class HeadsetBase { } } - private void log(String msg) { + public static int getAtInputCount() { + return sAtInputCount; + } + + private static void log(String msg) { Log.d(TAG, msg); } } diff --git a/core/java/android/bluetooth/IBluetoothHeadset.aidl b/core/java/android/bluetooth/IBluetoothHeadset.aidl index 582d4e3..5f42fd6 100644 --- a/core/java/android/bluetooth/IBluetoothHeadset.aidl +++ b/core/java/android/bluetooth/IBluetoothHeadset.aidl @@ -31,4 +31,5 @@ interface IBluetoothHeadset { boolean stopVoiceRecognition(); boolean setPriority(in String address, int priority); int getPriority(in String address); + int getBatteryUsageHint(); } diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java index ce6501c..249d9ba 100644 --- a/core/java/android/content/AbstractSyncableContentProvider.java +++ b/core/java/android/content/AbstractSyncableContentProvider.java @@ -147,7 +147,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro @Override public boolean onCreate() { if (isTemporary()) throw new IllegalStateException("onCreate() called for temp provider"); - mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(), mDatabaseName); + mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(), + mDatabaseName); mSyncState = new SyncStateContentProviderHelper(mOpenHelper); AccountMonitorListener listener = new AccountMonitorListener() { @@ -235,76 +236,147 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro return Collections.emptyList(); } - @Override - public final int update(final Uri url, final ContentValues values, - final String selection, final String[] selectionArgs) { + /** + * <p> + * Call mOpenHelper.getWritableDatabase() and mDb.beginTransaction(). + * {@link #endTransaction} MUST be called after calling this method. + * Those methods should be used like this: + * </p> + * + * <pre class="prettyprint"> + * boolean successful = false; + * beginTransaction(); + * try { + * // Do something related to mDb + * successful = true; + * return ret; + * } finally { + * endTransaction(successful); + * } + * </pre> + * + * @hide This method is dangerous from the view of database manipulation, though using + * this makes batch insertion/update/delete much faster. + */ + public final void beginTransaction() { mDb = mOpenHelper.getWritableDatabase(); mDb.beginTransaction(); + } + + /** + * <p> + * Call mDb.endTransaction(). If successful is true, try to call + * mDb.setTransactionSuccessful() before calling mDb.endTransaction(). + * This method MUST be used with {@link #beginTransaction()}. + * </p> + * + * @hide This method is dangerous from the view of database manipulation, though using + * this makes batch insertion/update/delete much faster. + */ + public final void endTransaction(boolean successful) { try { - if (isTemporary() && mSyncState.matches(url)) { - int numRows = mSyncState.asContentProvider().update( - url, values, selection, selectionArgs); + if (successful) { + // setTransactionSuccessful() must be called just once during opening the + // transaction. mDb.setTransactionSuccessful(); - return numRows; } + } finally { + mDb.endTransaction(); + } + } - int result = updateInternal(url, values, selection, selectionArgs); - mDb.setTransactionSuccessful(); + @Override + public final int update(final Uri uri, final ContentValues values, + final String selection, final String[] selectionArgs) { + boolean successful = false; + beginTransaction(); + try { + int ret = nonTransactionalUpdate(uri, values, selection, selectionArgs); + successful = true; + return ret; + } finally { + endTransaction(successful); + } + } - if (!isTemporary() && result > 0) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } + /** + * @hide + */ + public final int nonTransactionalUpdate(final Uri uri, final ContentValues values, + final String selection, final String[] selectionArgs) { + if (isTemporary() && mSyncState.matches(uri)) { + int numRows = mSyncState.asContentProvider().update( + uri, values, selection, selectionArgs); + return numRows; + } - return result; - } finally { - mDb.endTransaction(); + int result = updateInternal(uri, values, selection, selectionArgs); + if (!isTemporary() && result > 0) { + getContext().getContentResolver().notifyChange(uri, null /* observer */, + changeRequiresLocalSync(uri)); } + + return result; } @Override - public final int delete(final Uri url, final String selection, + public final int delete(final Uri uri, final String selection, final String[] selectionArgs) { - mDb = mOpenHelper.getWritableDatabase(); - mDb.beginTransaction(); + boolean successful = false; + beginTransaction(); try { - if (isTemporary() && mSyncState.matches(url)) { - int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs); - mDb.setTransactionSuccessful(); - return numRows; - } - int result = deleteInternal(url, selection, selectionArgs); - mDb.setTransactionSuccessful(); - if (!isTemporary() && result > 0) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } - return result; + int ret = nonTransactionalDelete(uri, selection, selectionArgs); + successful = true; + return ret; } finally { - mDb.endTransaction(); + endTransaction(successful); } } + /** + * @hide + */ + public final int nonTransactionalDelete(final Uri uri, final String selection, + final String[] selectionArgs) { + if (isTemporary() && mSyncState.matches(uri)) { + int numRows = mSyncState.asContentProvider().delete(uri, selection, selectionArgs); + return numRows; + } + int result = deleteInternal(uri, selection, selectionArgs); + if (!isTemporary() && result > 0) { + getContext().getContentResolver().notifyChange(uri, null /* observer */, + changeRequiresLocalSync(uri)); + } + return result; + } + @Override - public final Uri insert(final Uri url, final ContentValues values) { - mDb = mOpenHelper.getWritableDatabase(); - mDb.beginTransaction(); + public final Uri insert(final Uri uri, final ContentValues values) { + boolean successful = false; + beginTransaction(); try { - if (isTemporary() && mSyncState.matches(url)) { - Uri result = mSyncState.asContentProvider().insert(url, values); - mDb.setTransactionSuccessful(); - return result; - } - Uri result = insertInternal(url, values); - mDb.setTransactionSuccessful(); - if (!isTemporary() && result != null) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } - return result; + Uri ret = nonTransactionalInsert(uri, values); + successful = true; + return ret; } finally { - mDb.endTransaction(); + endTransaction(successful); + } + } + + /** + * @hide + */ + public final Uri nonTransactionalInsert(final Uri uri, final ContentValues values) { + if (isTemporary() && mSyncState.matches(uri)) { + Uri result = mSyncState.asContentProvider().insert(uri, values); + return result; + } + Uri result = insertInternal(uri, values); + if (!isTemporary() && result != null) { + getContext().getContentResolver().notifyChange(uri, null /* observer */, + changeRequiresLocalSync(uri)); } + return result; } @Override diff --git a/core/java/android/content/AbstractTableMerger.java b/core/java/android/content/AbstractTableMerger.java index 700f1d8..9c760d9 100644 --- a/core/java/android/content/AbstractTableMerger.java +++ b/core/java/android/content/AbstractTableMerger.java @@ -61,8 +61,10 @@ public abstract class AbstractTableMerger _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=?"; private static final String SELECT_BY_ID = BaseColumns._ID +"=?"; - private static final String SELECT_UNSYNCED = "" - + _SYNC_DIRTY + " > 0 and (" + _SYNC_ACCOUNT + "=? or " + _SYNC_ACCOUNT + " is null)"; + private static final String SELECT_UNSYNCED = + "(" + _SYNC_ACCOUNT + " IS NULL OR " + _SYNC_ACCOUNT + "=?) AND " + + "(" + _SYNC_ID + " IS NULL OR (" + _SYNC_DIRTY + " > 0 AND " + + _SYNC_VERSION + " IS NOT NULL))"; public AbstractTableMerger(SQLiteDatabase database, String table, Uri tableURL, String deletedTable, @@ -365,26 +367,32 @@ public abstract class AbstractTableMerger if (!TextUtils.isEmpty(localSyncID)) { // An existing server item has changed - boolean recordChanged = (localSyncVersion == null) || - !serverSyncVersion.equals(localSyncVersion); - if (recordChanged) { - if (localSyncDirty) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId - + " conflicts with local _sync_id " + localSyncID - + ", local _id " + localRowId); + // If serverSyncVersion is null, there is no edit URL; + // server won't let this change be written. + // Just hold onto it, I guess, in case the server permissions + // change later. + if (serverSyncVersion != null) { + boolean recordChanged = (localSyncVersion == null) || + !serverSyncVersion.equals(localSyncVersion); + if (recordChanged) { + if (localSyncDirty) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "remote record " + serverSyncId + + " conflicts with local _sync_id " + localSyncID + + ", local _id " + localRowId); + } + conflict = true; + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, + "remote record " + + serverSyncId + + " updates local _sync_id " + + localSyncID + ", local _id " + + localRowId); + } + update = true; } - conflict = true; - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "remote record " + - serverSyncId + - " updates local _sync_id " + - localSyncID + ", local _id " + - localRowId); - } - update = true; } } } else { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index f2ad248..9e37ae4 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -16,6 +16,7 @@ package android.content; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; @@ -233,6 +234,9 @@ public abstract class Context { /** Return the name of this application's package. */ public abstract String getPackageName(); + /** Return the full application info for this context's package. */ + public abstract ApplicationInfo getApplicationInfo(); + /** * {@hide} * Return the full path to this context's resource files. This is the ZIP files @@ -254,12 +258,20 @@ public abstract class Context { * <p>Note: this is not generally useful for applications, since they should * not be directly accessing the file system. * - * * @return String Path to the code and assets. */ public abstract String getPackageCodePath(); /** + * {@hide} + * Return the full path to the shared prefs file for the given prefs group name. + * + * <p>Note: this is not generally useful for applications, since they should + * not be directly accessing the file system. + */ + public abstract File getSharedPrefsFile(String name); + + /** * Retrieve and hold the contents of the preferences file 'name', returning * a SharedPreferences through which you can retrieve and modify its * values. Only one instance of the SharedPreferences object is returned @@ -527,16 +539,6 @@ public abstract class Context { public abstract int getWallpaperDesiredMinimumHeight(); /** - * Returns the scale in which the application will be drawn on the - * screen. This is usually 1.0f if the application supports the device's - * resolution/density. This will be 1.5f, for example, if the application - * that supports only 160 density runs on 240 density screen. - * - * @hide - */ - public abstract float getApplicationScale(); - - /** * Change the current system wallpaper to a bitmap. The given bitmap is * converted to a PNG and stored as the wallpaper. On success, the intent * {@link Intent#ACTION_WALLPAPER_CHANGED} is broadcast. @@ -1135,6 +1137,15 @@ public abstract class Context { public static final String NOTIFICATION_SERVICE = "notification"; /** * Use with {@link #getSystemService} to retrieve a + * {@link android.view.accessibility.AccessibilityManager} for giving the user + * feedback for UI events through the registered event listeners. + * + * @see #getSystemService + * @see android.view.accessibility.AccessibilityManager + */ + public static final String ACCESSIBILITY_SERVICE = "accessibility"; + /** + * Use with {@link #getSystemService} to retrieve a * {@link android.app.NotificationManager} for controlling keyguard. * * @see #getSystemService @@ -1643,6 +1654,13 @@ public abstract class Context { * with extreme care! */ public static final int CONTEXT_IGNORE_SECURITY = 0x00000002; + + /** + * Flag for use with {@link #createPackageContext}: a restricted context may + * disable specific features. For instance, a View associated with a restricted + * context would ignore particular XML attributes. + */ + public static final int CONTEXT_RESTRICTED = 0x00000004; /** * Return a new Context object for the given application name. This @@ -1671,4 +1689,15 @@ public abstract class Context { */ public abstract Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException; + + /** + * Indicates whether this Context is restricted. + * + * @return True if this Context is restricted, false otherwise. + * + * @see #CONTEXT_RESTRICTED + */ + public boolean isRestricted() { + return false; + } } diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 25b2cae..45a082a 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -16,6 +16,7 @@ package android.content; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Resources; @@ -120,6 +121,11 @@ public class ContextWrapper extends Context { } @Override + public ApplicationInfo getApplicationInfo() { + return mBase.getApplicationInfo(); + } + + @Override public String getPackageResourcePath() { return mBase.getPackageResourcePath(); } @@ -130,6 +136,11 @@ public class ContextWrapper extends Context { } @Override + public File getSharedPrefsFile(String name) { + return mBase.getSharedPrefsFile(name); + } + + @Override public SharedPreferences getSharedPreferences(String name, int mode) { return mBase.getSharedPreferences(name, mode); } @@ -420,11 +431,8 @@ public class ContextWrapper extends Context { return mBase.createPackageContext(packageName, flags); } - /** - * @hide - */ @Override - public float getApplicationScale() { - return mBase.getApplicationScale(); + public boolean isRestricted() { + return mBase.isRestricted(); } } diff --git a/core/java/android/content/IIntentReceiver.aidl b/core/java/android/content/IIntentReceiver.aidl new file mode 100755 index 0000000..443db2d --- /dev/null +++ b/core/java/android/content/IIntentReceiver.aidl @@ -0,0 +1,33 @@ +/* + * 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.content; + +import android.content.Intent; +import android.os.Bundle; + +/** + * System private API for dispatching intent broadcasts. This is given to the + * activity manager as part of registering for an intent broadcasts, and is + * called when it receives intents. + * + * {@hide} + */ +oneway interface IIntentReceiver { + void performReceive(in Intent intent, int resultCode, + String data, in Bundle extras, boolean ordered); +} + diff --git a/core/java/android/content/IIntentSender.aidl b/core/java/android/content/IIntentSender.aidl new file mode 100644 index 0000000..b7da472 --- /dev/null +++ b/core/java/android/content/IIntentSender.aidl @@ -0,0 +1,26 @@ +/* + * 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.content; + +import android.content.IIntentReceiver; +import android.content.Intent; + +/** @hide */ +interface IIntentSender { + int send(int code, in Intent intent, String resolvedType, + IIntentReceiver finishedReceiver); +} diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 24262f5..263f927 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -240,35 +240,35 @@ import java.util.Set; * * <activity class=".NotesList" android:label="@string/title_notes_list"> * <intent-filter> - * <action android:value="android.intent.action.MAIN" /> - * <category android:value="android.intent.category.LAUNCHER" /> + * <action android:name="android.intent.action.MAIN" /> + * <category android:name="android.intent.category.LAUNCHER" /> * </intent-filter> * <intent-filter> - * <action android:value="android.intent.action.VIEW" /> - * <action android:value="android.intent.action.EDIT" /> - * <action android:value="android.intent.action.PICK" /> - * <category android:value="android.intent.category.DEFAULT" /> - * <type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> + * <action android:name="android.intent.action.VIEW" /> + * <action android:name="android.intent.action.EDIT" /> + * <action android:name="android.intent.action.PICK" /> + * <category android:name="android.intent.category.DEFAULT" /> + * <data android:mimeType="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> * </intent-filter> * <intent-filter> - * <action android:value="android.intent.action.GET_CONTENT" /> - * <category android:value="android.intent.category.DEFAULT" /> - * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * <action android:name="android.intent.action.GET_CONTENT" /> + * <category android:name="android.intent.category.DEFAULT" /> + * <data android:mimeType="vnd.android.cursor.item/<i>vnd.google.note</i>" /> * </intent-filter> * </activity> * * <activity class=".NoteEditor" android:label="@string/title_note"> * <intent-filter android:label="@string/resolve_edit"> - * <action android:value="android.intent.action.VIEW" /> - * <action android:value="android.intent.action.EDIT" /> - * <category android:value="android.intent.category.DEFAULT" /> - * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * <action android:name="android.intent.action.VIEW" /> + * <action android:name="android.intent.action.EDIT" /> + * <category android:name="android.intent.category.DEFAULT" /> + * <data android:mimeType="vnd.android.cursor.item/<i>vnd.google.note</i>" /> * </intent-filter> * * <intent-filter> - * <action android:value="android.intent.action.INSERT" /> - * <category android:value="android.intent.category.DEFAULT" /> - * <type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> + * <action android:name="android.intent.action.INSERT" /> + * <category android:name="android.intent.category.DEFAULT" /> + * <data android:mimeType="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> * </intent-filter> * * </activity> @@ -276,11 +276,11 @@ import java.util.Set; * <activity class=".TitleEditor" android:label="@string/title_edit_title" * android:theme="@android:style/Theme.Dialog"> * <intent-filter android:label="@string/resolve_title"> - * <action android:value="<i>com.android.notepad.action.EDIT_TITLE</i>" /> - * <category android:value="android.intent.category.DEFAULT" /> - * <category android:value="android.intent.category.ALTERNATIVE" /> - * <category android:value="android.intent.category.SELECTED_ALTERNATIVE" /> - * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * <action android:name="<i>com.android.notepad.action.EDIT_TITLE</i>" /> + * <category android:name="android.intent.category.DEFAULT" /> + * <category android:name="android.intent.category.ALTERNATIVE" /> + * <category android:name="android.intent.category.SELECTED_ALTERNATIVE" /> + * <data android:mimeType="vnd.android.cursor.item/<i>vnd.google.note</i>" /> * </intent-filter> * </activity> * @@ -294,8 +294,8 @@ import java.util.Set; * <ol> * <li><pre> * <intent-filter> - * <action android:value="{@link #ACTION_MAIN android.intent.action.MAIN}" /> - * <category android:value="{@link #CATEGORY_LAUNCHER android.intent.category.LAUNCHER}" /> + * <action android:name="{@link #ACTION_MAIN android.intent.action.MAIN}" /> + * <category android:name="{@link #CATEGORY_LAUNCHER android.intent.category.LAUNCHER}" /> * </intent-filter></pre> * <p>This provides a top-level entry into the NotePad application: the standard * MAIN action is a main entry point (not requiring any other information in @@ -303,11 +303,11 @@ import java.util.Set; * listed in the application launcher.</p> * <li><pre> * <intent-filter> - * <action android:value="{@link #ACTION_VIEW android.intent.action.VIEW}" /> - * <action android:value="{@link #ACTION_EDIT android.intent.action.EDIT}" /> - * <action android:value="{@link #ACTION_PICK android.intent.action.PICK}" /> - * <category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> - * <type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> + * <action android:name="{@link #ACTION_VIEW android.intent.action.VIEW}" /> + * <action android:name="{@link #ACTION_EDIT android.intent.action.EDIT}" /> + * <action android:name="{@link #ACTION_PICK android.intent.action.PICK}" /> + * <category android:name="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> + * <data mimeType:name="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> * </intent-filter></pre> * <p>This declares the things that the activity can do on a directory of * notes. The type being supported is given with the <type> tag, where @@ -322,9 +322,9 @@ import java.util.Set; * activity when its component name is not explicitly specified.</p> * <li><pre> * <intent-filter> - * <action android:value="{@link #ACTION_GET_CONTENT android.intent.action.GET_CONTENT}" /> - * <category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> - * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * <action android:name="{@link #ACTION_GET_CONTENT android.intent.action.GET_CONTENT}" /> + * <category android:name="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> + * <data android:mimeType="vnd.android.cursor.item/<i>vnd.google.note</i>" /> * </intent-filter></pre> * <p>This filter describes the ability return to the caller a note selected by * the user without needing to know where it came from. The data type @@ -371,10 +371,10 @@ import java.util.Set; * <ol> * <li><pre> * <intent-filter android:label="@string/resolve_edit"> - * <action android:value="{@link #ACTION_VIEW android.intent.action.VIEW}" /> - * <action android:value="{@link #ACTION_EDIT android.intent.action.EDIT}" /> - * <category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> - * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * <action android:name="{@link #ACTION_VIEW android.intent.action.VIEW}" /> + * <action android:name="{@link #ACTION_EDIT android.intent.action.EDIT}" /> + * <category android:name="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> + * <data android:mimeType="vnd.android.cursor.item/<i>vnd.google.note</i>" /> * </intent-filter></pre> * <p>The first, primary, purpose of this activity is to let the user interact * with a single note, as decribed by the MIME type @@ -384,9 +384,9 @@ import java.util.Set; * specifying its component.</p> * <li><pre> * <intent-filter> - * <action android:value="{@link #ACTION_INSERT android.intent.action.INSERT}" /> - * <category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> - * <type android:value="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> + * <action android:name="{@link #ACTION_INSERT android.intent.action.INSERT}" /> + * <category android:name="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> + * <data android:mimeType="vnd.android.cursor.dir/<i>vnd.google.note</i>" /> * </intent-filter></pre> * <p>The secondary use of this activity is to insert a new note entry into * an existing directory of notes. This is used when the user creates a new @@ -422,11 +422,11 @@ import java.util.Set; * * <pre> * <intent-filter android:label="@string/resolve_title"> - * <action android:value="<i>com.android.notepad.action.EDIT_TITLE</i>" /> - * <category android:value="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> - * <category android:value="{@link #CATEGORY_ALTERNATIVE android.intent.category.ALTERNATIVE}" /> - * <category android:value="{@link #CATEGORY_SELECTED_ALTERNATIVE android.intent.category.SELECTED_ALTERNATIVE}" /> - * <type android:value="vnd.android.cursor.item/<i>vnd.google.note</i>" /> + * <action android:name="<i>com.android.notepad.action.EDIT_TITLE</i>" /> + * <category android:name="{@link #CATEGORY_DEFAULT android.intent.category.DEFAULT}" /> + * <category android:name="{@link #CATEGORY_ALTERNATIVE android.intent.category.ALTERNATIVE}" /> + * <category android:name="{@link #CATEGORY_SELECTED_ALTERNATIVE android.intent.category.SELECTED_ALTERNATIVE}" /> + * <data android:mimeType="vnd.android.cursor.item/<i>vnd.google.note</i>" /> * </intent-filter></pre> * * <p>In the single intent template here, we @@ -509,8 +509,8 @@ import java.util.Set; * <li> {@link #ACTION_UID_REMOVED} * <li> {@link #ACTION_BATTERY_CHANGED} * <li> {@link #ACTION_POWER_CONNECTED} - * <li> {@link #ACTION_POWER_DISCONNECTED} - * <li> {@link #ACTION_SHUTDOWN} + * <li> {@link #ACTION_POWER_DISCONNECTED} + * <li> {@link #ACTION_SHUTDOWN} * </ul> * * <h3>Standard Categories</h3> @@ -915,6 +915,23 @@ public class Intent implements Parcelable { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_SEND = "android.intent.action.SEND"; /** + * Activity Action: Deliver multiple data to someone else. + * <p> + * Like ACTION_SEND, except the data is multiple. + * <p> + * Input: {@link #getType} is the MIME type of the data being sent. + * get*ArrayListExtra can have either a {@link #EXTRA_TEXT} or {@link + * #EXTRA_STREAM} field, containing the data to be sent. + * <p> + * Optional standard extras, which may be interpreted by some recipients as + * appropriate, are: {@link #EXTRA_EMAIL}, {@link #EXTRA_CC}, + * {@link #EXTRA_BCC}, {@link #EXTRA_SUBJECT}. + * <p> + * Output: nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SEND_MULTIPLE = "android.intent.action.SEND_MULTIPLE"; + /** * Activity Action: Handle an incoming phone call. * <p>Input: nothing. * <p>Output: nothing. @@ -1059,6 +1076,36 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_APP_ERROR = "android.intent.action.APP_ERROR"; + + /** + * Activity Action: Show power usage information to the user. + * <p>Input: Nothing. + * <p>Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_POWER_USAGE_SUMMARY = "android.intent.action.POWER_USAGE_SUMMARY"; + + /** + * Activity Action: Setup wizard to launch after a platform update. This + * activity should have a string meta-data field associated with it, + * {@link #METADATA_SETUP_VERSION}, which defines the current version of + * the platform for setup. The activity will be launched only if + * {@link android.provider.Settings.Secure#LAST_SETUP_SHOWN} is not the + * same value. + * <p>Input: Nothing. + * <p>Output: Nothing. + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_UPGRADE_SETUP = "android.intent.action.UPGRADE_SETUP"; + + /** + * A string associated with a {@link #ACTION_UPGRADE_SETUP} activity + * describing the last run version of the platform that was setup. + * @hide + */ + public static final String METADATA_SETUP_VERSION = "android.SETUP_VERSION"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent broadcast actions (see action variable). @@ -1264,6 +1311,13 @@ public class Intent implements Parcelable { @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_BATTERY_LOW = "android.intent.action.BATTERY_LOW"; /** + * Broadcast Action: Indicates the battery is now okay after being low. + * This will be sent after {@link #ACTION_BATTERY_LOW} once the battery has + * gone back up to an okay state. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_BATTERY_OKAY = "android.intent.action.BATTERY_OKAY"; + /** * Broadcast Action: External power has been connected to the device. * This is intended for applications that wish to register specifically to this notification. * Unlike ACTION_BATTERY_CHANGED, applications will be woken for this and so do not have to @@ -1277,10 +1331,10 @@ public class Intent implements Parcelable { * This is intended for applications that wish to register specifically to this notification. * Unlike ACTION_BATTERY_CHANGED, applications will be woken for this and so do not have to * stay active to receive this notification. This action can be used to implement actions - * that wait until power is available to trigger. + * that wait until power is available to trigger. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_POWER_DISCONNECTED = "android.intent.action.ACTION_POWER_DISCONNECTED"; + public static final String ACTION_POWER_DISCONNECTED = "android.intent.action.ACTION_POWER_DISCONNECTED"; /** * Broadcast Action: Device is shutting down. * This is broadcast when the device is being shut down (completely turned @@ -1289,7 +1343,7 @@ public class Intent implements Parcelable { * to handle this, since the forground activity will be paused as well. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_SHUTDOWN = "android.intent.action.ACTION_SHUTDOWN"; + public static final String ACTION_SHUTDOWN = "android.intent.action.ACTION_SHUTDOWN"; /** * Broadcast Action: Indicates low memory condition on the device */ @@ -1552,6 +1606,16 @@ public class Intent implements Parcelable { public static final String ACTION_REBOOT = "android.intent.action.REBOOT"; + /** + * @hide + * TODO: This will be unhidden in a later CL. + * Broadcast Action: The TextToSpeech synthesizer has completed processing + * all of the text in the speech queue. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_TTS_QUEUE_PROCESSING_COMPLETED = + "android.intent.action.TTS_QUEUE_PROCESSING_COMPLETED"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent categories (see addCategory()). @@ -1791,23 +1855,23 @@ public class Intent implements Parcelable { * delivered. */ public static final String EXTRA_ALARM_COUNT = "android.intent.extra.ALARM_COUNT"; - + /** * Used as a parcelable extra field in {@link #ACTION_APP_ERROR}, containing * the bug report. - * + * * @hide */ public static final String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT"; /** - * Used as a string extra field when sending an intent to PackageInstaller to install a + * Used as a string extra field when sending an intent to PackageInstaller to install a * package. Specifies the installer package name; this package will receive the * {@link #ACTION_APP_ERROR} intent. - * + * * @hide */ - public static final String EXTRA_INSTALLER_PACKAGE_NAME + public static final String EXTRA_INSTALLER_PACKAGE_NAME = "android.intent.extra.INSTALLER_PACKAGE_NAME"; // --------------------------------------------------------------------- @@ -2040,10 +2104,25 @@ public class Intent implements Parcelable { public static final int FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT = 0x20000000; // --------------------------------------------------------------------- + // --------------------------------------------------------------------- + // toUri() and parseUri() options. + + /** + * Flag for use with {@link #toUri} and {@link #parseUri}: the URI string + * always has the "intent:" scheme. This syntax can be used when you want + * to later disambiguate between URIs that are intended to describe an + * Intent vs. all others that should be treated as raw URIs. When used + * with {@link #parseUri}, any other scheme will result in a generic + * VIEW action for that raw URI. + */ + public static final int URI_INTENT_SCHEME = 1<<0; + + // --------------------------------------------------------------------- private String mAction; private Uri mData; private String mType; + private String mPackage; private ComponentName mComponent; private int mFlags; private HashSet<String> mCategories; @@ -2064,6 +2143,7 @@ public class Intent implements Parcelable { this.mAction = o.mAction; this.mData = o.mData; this.mType = o.mType; + this.mPackage = o.mPackage; this.mComponent = o.mComponent; this.mFlags = o.mFlags; if (o.mCategories != null) { @@ -2083,6 +2163,7 @@ public class Intent implements Parcelable { this.mAction = o.mAction; this.mData = o.mData; this.mType = o.mType; + this.mPackage = o.mPackage; this.mComponent = o.mComponent; if (o.mCategories != null) { this.mCategories = new HashSet<String>(o.mCategories); @@ -2183,23 +2264,50 @@ public class Intent implements Parcelable { } /** + * Call {@link #parseUri} with 0 flags. + * @deprecated Use {@link #parseUri} instead. + */ + @Deprecated + public static Intent getIntent(String uri) throws URISyntaxException { + return parseUri(uri, 0); + } + + /** * Create an intent from a URI. This URI may encode the action, - * category, and other intent fields, if it was returned by toURI(). If - * the Intent was not generate by toURI(), its data will be the entire URI - * and its action will be ACTION_VIEW. + * category, and other intent fields, if it was returned by + * {@link #toUri}.. If the Intent was not generate by toUri(), its data + * will be the entire URI and its action will be ACTION_VIEW. * * <p>The URI given here must not be relative -- that is, it must include * the scheme and full path. * * @param uri The URI to turn into an Intent. + * @param flags Additional processing flags. Either 0 or * * @return Intent The newly created Intent object. * - * @see #toURI + * @throws URISyntaxException Throws URISyntaxError if the basic URI syntax + * it bad (as parsed by the Uri class) or the Intent data within the + * URI is invalid. + * + * @see #toUri */ - public static Intent getIntent(String uri) throws URISyntaxException { + public static Intent parseUri(String uri, int flags) throws URISyntaxException { int i = 0; try { + // Validate intent scheme for if requested. + if ((flags&URI_INTENT_SCHEME) != 0) { + if (!uri.startsWith("intent:")) { + Intent intent = new Intent(ACTION_VIEW); + try { + intent.setData(Uri.parse(uri)); + } catch (IllegalArgumentException e) { + throw new URISyntaxException(uri, e.getMessage()); + } + return intent; + } + } + // simple case i = uri.lastIndexOf("#"); if (i == -1) return new Intent(ACTION_VIEW, Uri.parse(uri)); @@ -2211,16 +2319,15 @@ public class Intent implements Parcelable { Intent intent = new Intent(ACTION_VIEW); // fetch data part, if present - if (i > 0) { - intent.mData = Uri.parse(uri.substring(0, i)); - } + String data = i >= 0 ? uri.substring(0, i) : null; + String scheme = null; i += "#Intent;".length(); // loop over contents of Intent, all name=value; while (!uri.startsWith("end", i)) { int eq = uri.indexOf('=', i); int semi = uri.indexOf(';', eq); - String value = uri.substring(eq + 1, semi); + String value = Uri.decode(uri.substring(eq + 1, semi)); // action if (uri.startsWith("action=", i)) { @@ -2242,15 +2349,24 @@ public class Intent implements Parcelable { intent.mFlags = Integer.decode(value).intValue(); } + // package + else if (uri.startsWith("package=", i)) { + intent.mPackage = value; + } + // component else if (uri.startsWith("component=", i)) { intent.mComponent = ComponentName.unflattenFromString(value); } + // scheme + else if (uri.startsWith("scheme=", i)) { + scheme = value; + } + // extra else { String key = Uri.decode(uri.substring(i + 2, eq)); - value = Uri.decode(value); // create Bundle if it doesn't already exist if (intent.mExtras == null) intent.mExtras = new Bundle(); Bundle b = intent.mExtras; @@ -2271,6 +2387,23 @@ public class Intent implements Parcelable { i = semi + 1; } + if (data != null) { + if (data.startsWith("intent:")) { + data = data.substring(7); + if (scheme != null) { + data = scheme + ':' + data; + } + } + + if (data.length() > 0) { + try { + intent.mData = Uri.parse(data); + } catch (IllegalArgumentException e) { + throw new URISyntaxException(uri, e.getMessage()); + } + } + } + return intent; } catch (IndexOutOfBoundsException e) { @@ -3084,6 +3217,20 @@ public class Intent implements Parcelable { } /** + * Retrieve the application package name this Intent is limited to. When + * resolving an Intent, if non-null this limits the resolution to only + * components in the given application package. + * + * @return The name of the application package for the Intent. + * + * @see #resolveActivity + * @see #setPackage + */ + public String getPackage() { + return mPackage; + } + + /** * Retrieve the concrete component associated with the intent. When receiving * an intent, this is the component that was found to best handle it (that is, * yourself) and will always be non-null; in all other cases it will be @@ -3118,6 +3265,9 @@ public class Intent implements Parcelable { * <p>If {@link #addCategory} has added any categories, the activity must * handle ALL of the categories specified. * + * <p>If {@link #getPackage} is non-NULL, only activity components in + * that application package will be considered. + * * <p>If there are no activities that satisfy all of these conditions, a * null string is returned. * @@ -3239,7 +3389,7 @@ public class Intent implements Parcelable { * only specify a type and not data, for example to indicate the type of * data to return. This method automatically clears any data that was * previously set by {@link #setData}. - * + * * <p><em>Note: MIME type matching in the Android framework is * case-sensitive, unlike formal RFC MIME types. As a result, * you should always write your MIME types with lower case letters, @@ -4089,6 +4239,27 @@ public class Intent implements Parcelable { } /** + * (Usually optional) Set an explicit application package name that limits + * the components this Intent will resolve to. If left to the default + * value of null, all components in all applications will considered. + * If non-null, the Intent can only match the components in the given + * application package. + * + * @param packageName The name of the application package to handle the + * intent, or null to allow any application package. + * + * @return Returns the same Intent object, for chaining multiple calls + * into a single statement. + * + * @see #getPackage + * @see #resolveActivity + */ + public Intent setPackage(String packageName) { + mPackage = packageName; + return this; + } + + /** * (Usually optional) Explicitly set the component to handle the intent. * If left with the default value of null, the system will determine the * appropriate class to use based on the other fields (action, data, @@ -4200,6 +4371,12 @@ public class Intent implements Parcelable { public static final int FILL_IN_COMPONENT = 1<<3; /** + * Use with {@link #fillIn} to allow the current package value to be + * overwritten, even if it is already set. + */ + public static final int FILL_IN_PACKAGE = 1<<4; + + /** * Copy the contents of <var>other</var> in to this object, but only * where fields are not defined by this object. For purposes of a field * being defined, the following pieces of data in the Intent are @@ -4210,14 +4387,15 @@ public class Intent implements Parcelable { * <li> data URI and MIME type, as set by {@link #setData(Uri)}, * {@link #setType(String)}, or {@link #setDataAndType(Uri, String)}. * <li> categories, as set by {@link #addCategory}. + * <li> package, as set by {@link #setPackage}. * <li> component, as set by {@link #setComponent(ComponentName)} or * related methods. * <li> each top-level name in the associated extras. * </ul> * * <p>In addition, you can use the {@link #FILL_IN_ACTION}, - * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, and - * {@link #FILL_IN_COMPONENT} to override the restriction where the + * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, {@link #FILL_IN_PACKAGE}, + * and {@link #FILL_IN_COMPONENT} to override the restriction where the * corresponding field will not be replaced if it is already set. * * <p>For example, consider Intent A with {data="foo", categories="bar"} @@ -4233,32 +4411,39 @@ public class Intent implements Parcelable { * @param flags Options to control which fields can be filled in. * * @return Returns a bit mask of {@link #FILL_IN_ACTION}, - * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, and - * {@link #FILL_IN_COMPONENT} indicating which fields were changed. + * {@link #FILL_IN_DATA}, {@link #FILL_IN_CATEGORIES}, {@link #FILL_IN_PACKAGE}, + * and {@link #FILL_IN_COMPONENT} indicating which fields were changed. */ public int fillIn(Intent other, int flags) { int changes = 0; - if ((mAction == null && other.mAction == null) - || (flags&FILL_IN_ACTION) != 0) { + if (other.mAction != null + && (mAction == null || (flags&FILL_IN_ACTION) != 0)) { mAction = other.mAction; changes |= FILL_IN_ACTION; } - if ((mData == null && mType == null && - (other.mData != null || other.mType != null)) - || (flags&FILL_IN_DATA) != 0) { + if ((other.mData != null || other.mType != null) + && ((mData == null && mType == null) + || (flags&FILL_IN_DATA) != 0)) { mData = other.mData; mType = other.mType; changes |= FILL_IN_DATA; } - if ((mCategories == null && other.mCategories == null) - || (flags&FILL_IN_CATEGORIES) != 0) { + if (other.mCategories != null + && (mCategories == null || (flags&FILL_IN_CATEGORIES) != 0)) { if (other.mCategories != null) { mCategories = new HashSet<String>(other.mCategories); } changes |= FILL_IN_CATEGORIES; } - if ((mComponent == null && other.mComponent == null) - || (flags&FILL_IN_COMPONENT) != 0) { + if (other.mPackage != null + && (mPackage == null || (flags&FILL_IN_PACKAGE) != 0)) { + mPackage = other.mPackage; + changes |= FILL_IN_PACKAGE; + } + // Component is special: it can -only- be set if explicitly allowed, + // since otherwise the sender could force the intent somewhere the + // originator didn't intend. + if (other.mComponent != null && (flags&FILL_IN_COMPONENT) != 0) { mComponent = other.mComponent; changes |= FILL_IN_COMPONENT; } @@ -4373,6 +4558,17 @@ public class Intent implements Parcelable { } } } + if (mPackage != other.mPackage) { + if (mPackage != null) { + if (!mPackage.equals(other.mPackage)) { + return false; + } + } else { + if (!other.mPackage.equals(mPackage)) { + return false; + } + } + } if (mComponent != other.mComponent) { if (mComponent != null) { if (!mComponent.equals(other.mComponent)) { @@ -4418,6 +4614,9 @@ public class Intent implements Parcelable { if (mType != null) { code += mType.hashCode(); } + if (mPackage != null) { + code += mPackage.hashCode(); + } if (mComponent != null) { code += mComponent.hashCode(); } @@ -4444,7 +4643,7 @@ public class Intent implements Parcelable { toShortString(b, comp, extras); return b.toString(); } - + /** @hide */ public void toShortString(StringBuilder b, boolean comp, boolean extras) { boolean first = true; @@ -4488,6 +4687,13 @@ public class Intent implements Parcelable { first = false; b.append("flg=0x").append(Integer.toHexString(mFlags)); } + if (mPackage != null) { + if (!first) { + b.append(' '); + } + first = false; + b.append("pkg=").append(mPackage); + } if (comp && mComponent != null) { if (!first) { b.append(' '); @@ -4504,28 +4710,87 @@ public class Intent implements Parcelable { } } + /** + * Call {@link #toUri} with 0 flags. + * @deprecated Use {@link #toUri} instead. + */ + @Deprecated public String toURI() { + return toUri(0); + } + + /** + * Convert this Intent into a String holding a URI representation of it. + * The returned URI string has been properly URI encoded, so it can be + * used with {@link Uri#parse Uri.parse(String)}. The URI contains the + * Intent's data as the base URI, with an additional fragment describing + * the action, categories, type, flags, package, component, and extras. + * + * <p>You can convert the returned string back to an Intent with + * {@link #getIntent}. + * + * @param flags Additional operating flags. Either 0 or + * {@link #URI_INTENT_SCHEME}. + * + * @return Returns a URI encoding URI string describing the entire contents + * of the Intent. + */ + public String toUri(int flags) { StringBuilder uri = new StringBuilder(128); - if (mData != null) uri.append(mData.toString()); + String scheme = null; + if (mData != null) { + String data = mData.toString(); + if ((flags&URI_INTENT_SCHEME) != 0) { + final int N = data.length(); + for (int i=0; i<N; i++) { + char c = data.charAt(i); + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + || c == '.' || c == '-') { + continue; + } + if (c == ':' && i > 0) { + // Valid scheme. + scheme = data.substring(0, i); + uri.append("intent:"); + data = data.substring(i+1); + break; + } + + // No scheme. + break; + } + } + uri.append(data); + + } else if ((flags&URI_INTENT_SCHEME) != 0) { + uri.append("intent:"); + } uri.append("#Intent;"); + if (scheme != null) { + uri.append("scheme=").append(scheme).append(';'); + } if (mAction != null) { - uri.append("action=").append(mAction).append(';'); + uri.append("action=").append(Uri.encode(mAction)).append(';'); } if (mCategories != null) { for (String category : mCategories) { - uri.append("category=").append(category).append(';'); + uri.append("category=").append(Uri.encode(category)).append(';'); } } if (mType != null) { - uri.append("type=").append(mType).append(';'); + uri.append("type=").append(Uri.encode(mType, "/")).append(';'); } if (mFlags != 0) { uri.append("launchFlags=0x").append(Integer.toHexString(mFlags)).append(';'); } + if (mPackage != null) { + uri.append("package=").append(Uri.encode(mPackage)).append(';'); + } if (mComponent != null) { - uri.append("component=").append(mComponent.flattenToShortString()).append(';'); + uri.append("component=").append(Uri.encode( + mComponent.flattenToShortString(), "/")).append(';'); } if (mExtras != null) { for (String key : mExtras.keySet()) { @@ -4567,6 +4832,7 @@ public class Intent implements Parcelable { Uri.writeToParcel(out, mData); out.writeString(mType); out.writeInt(mFlags); + out.writeString(mPackage); ComponentName.writeToParcel(mComponent, out); if (mCategories != null) { @@ -4600,6 +4866,7 @@ public class Intent implements Parcelable { mData = Uri.CREATOR.createFromParcel(in); mType = in.readString(); mFlags = in.readInt(); + mPackage = in.readString(); mComponent = ComponentName.readFromParcel(in); int N = in.readInt(); diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index e5c5dc8..365f269 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -366,6 +366,7 @@ public class IntentFilter implements Parcelable { throws MalformedMimeTypeException { mPriority = 0; mActions = new ArrayList<String>(); + addAction(action); addDataType(dataType); } diff --git a/core/java/android/content/IntentSender.aidl b/core/java/android/content/IntentSender.aidl new file mode 100644 index 0000000..741bc8c --- /dev/null +++ b/core/java/android/content/IntentSender.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +parcelable IntentSender; diff --git a/core/java/android/content/IntentSender.java b/core/java/android/content/IntentSender.java new file mode 100644 index 0000000..4da49d9 --- /dev/null +++ b/core/java/android/content/IntentSender.java @@ -0,0 +1,255 @@ +/* + * 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.content; + +import android.content.Context; +import android.content.Intent; +import android.content.IIntentSender; +import android.content.IIntentReceiver; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.Handler; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AndroidException; + + +/** + * A description of an Intent and target action to perform with it. + * The returned object can be + * handed to other applications so that they can perform the action you + * described on your behalf at a later time. + * + * <p>By giving a IntentSender to another application, + * you are granting it the right to perform the operation you have specified + * as if the other application was yourself (with the same permissions and + * identity). As such, you should be careful about how you build the IntentSender: + * often, for example, the base Intent you supply will have the component + * name explicitly set to one of your own components, to ensure it is ultimately + * sent there and nowhere else. + * + * <p>A IntentSender itself is simply a reference to a token maintained by + * the system describing the original data used to retrieve it. This means + * that, even if its owning application's process is killed, the + * IntentSender itself will remain usable from other processes that + * have been given it. If the creating application later re-retrieves the + * same kind of IntentSender (same operation, same Intent action, data, + * categories, and components, and same flags), it will receive a IntentSender + * representing the same token if that is still valid. + * + */ +public class IntentSender implements Parcelable { + private final IIntentSender mTarget; + + /** + * Exception thrown when trying to send through a PendingIntent that + * has been canceled or is otherwise no longer able to execute the request. + */ + public static class SendIntentException extends AndroidException { + public SendIntentException() { + } + + public SendIntentException(String name) { + super(name); + } + + public SendIntentException(Exception cause) { + super(cause); + } + } + + /** + * Callback interface for discovering when a send operation has + * completed. Primarily for use with a IntentSender that is + * performing a broadcast, this provides the same information as + * calling {@link Context#sendOrderedBroadcast(Intent, String, + * android.content.BroadcastReceiver, Handler, int, String, Bundle) + * Context.sendBroadcast()} with a final BroadcastReceiver. + */ + public interface OnFinished { + /** + * Called when a send operation as completed. + * + * @param IntentSender The IntentSender this operation was sent through. + * @param intent The original Intent that was sent. + * @param resultCode The final result code determined by the send. + * @param resultData The final data collected by a broadcast. + * @param resultExtras The final extras collected by a broadcast. + */ + void onSendFinished(IntentSender IntentSender, Intent intent, + int resultCode, String resultData, Bundle resultExtras); + } + + private static class FinishedDispatcher extends IIntentReceiver.Stub + implements Runnable { + private final IntentSender mIntentSender; + private final OnFinished mWho; + private final Handler mHandler; + private Intent mIntent; + private int mResultCode; + private String mResultData; + private Bundle mResultExtras; + FinishedDispatcher(IntentSender pi, OnFinished who, Handler handler) { + mIntentSender = pi; + mWho = who; + mHandler = handler; + } + public void performReceive(Intent intent, int resultCode, + String data, Bundle extras, boolean serialized) { + mIntent = intent; + mResultCode = resultCode; + mResultData = data; + mResultExtras = extras; + if (mHandler == null) { + run(); + } else { + mHandler.post(this); + } + } + public void run() { + mWho.onSendFinished(mIntentSender, mIntent, mResultCode, + mResultData, mResultExtras); + } + } + + /** + * Perform the operation associated with this IntentSender, allowing the + * caller to specify information about the Intent to use and be notified + * when the send has completed. + * + * @param context The Context of the caller. This may be null if + * <var>intent</var> is also null. + * @param code Result code to supply back to the IntentSender's target. + * @param intent Additional Intent data. See {@link Intent#fillIn + * Intent.fillIn()} for information on how this is applied to the + * original Intent. Use null to not modify the original Intent. + * @param onFinished The object to call back on when the send has + * completed, or null for no callback. + * @param handler Handler identifying the thread on which the callback + * should happen. If null, the callback will happen from the thread + * pool of the process. + * + * + * @throws SendIntentException Throws CanceledIntentException if the IntentSender + * is no longer allowing more intents to be sent through it. + */ + public void sendIntent(Context context, int code, Intent intent, + OnFinished onFinished, Handler handler) throws SendIntentException { + try { + String resolvedType = intent != null ? + intent.resolveTypeIfNeeded(context.getContentResolver()) + : null; + int res = mTarget.send(code, intent, resolvedType, + onFinished != null + ? new FinishedDispatcher(this, onFinished, handler) + : null); + if (res < 0) { + throw new SendIntentException(); + } + } catch (RemoteException e) { + throw new SendIntentException(); + } + } + + /** + * Comparison operator on two IntentSender objects, such that true + * is returned then they both represent the same operation from the + * same package. + */ + @Override + public boolean equals(Object otherObj) { + if (otherObj instanceof IntentSender) { + return mTarget.asBinder().equals(((IntentSender)otherObj) + .mTarget.asBinder()); + } + return false; + } + + @Override + public int hashCode() { + return mTarget.asBinder().hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("IntentSender{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(": "); + sb.append(mTarget != null ? mTarget.asBinder() : null); + sb.append('}'); + return sb.toString(); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeStrongBinder(mTarget.asBinder()); + } + + public static final Parcelable.Creator<IntentSender> CREATOR + = new Parcelable.Creator<IntentSender>() { + public IntentSender createFromParcel(Parcel in) { + IBinder target = in.readStrongBinder(); + return target != null ? new IntentSender(target) : null; + } + + public IntentSender[] newArray(int size) { + return new IntentSender[size]; + } + }; + + /** + * Convenience function for writing either a IntentSender or null pointer to + * a Parcel. You must use this with {@link #readIntentSenderOrNullFromParcel} + * for later reading it. + * + * @param sender The IntentSender to write, or null. + * @param out Where to write the IntentSender. + */ + public static void writeIntentSenderOrNullToParcel(IntentSender sender, + Parcel out) { + out.writeStrongBinder(sender != null ? sender.mTarget.asBinder() + : null); + } + + /** + * Convenience function for reading either a Messenger or null pointer from + * a Parcel. You must have previously written the Messenger with + * {@link #writeIntentSenderOrNullToParcel}. + * + * @param in The Parcel containing the written Messenger. + * + * @return Returns the Messenger read from the Parcel, or null if null had + * been written. + */ + public static IntentSender readIntentSenderOrNullFromParcel(Parcel in) { + IBinder b = in.readStrongBinder(); + return b != null ? new IntentSender(b) : null; + } + + protected IntentSender(IIntentSender target) { + mTarget = target; + } + + protected IntentSender(IBinder target) { + mTarget = IIntentSender.Stub.asInterface(target); + } +} diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java index 9c25e73..f781e0d 100644 --- a/core/java/android/content/SyncStorageEngine.java +++ b/core/java/android/content/SyncStorageEngine.java @@ -24,6 +24,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; +import android.backup.IBackupManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; @@ -35,6 +36,7 @@ import android.os.Message; import android.os.Parcel; import android.os.RemoteCallbackList; import android.os.RemoteException; +import android.os.ServiceManager; import android.util.Log; import android.util.SparseArray; import android.util.Xml; @@ -351,8 +353,18 @@ public class SyncStorageEngine extends Handler { } } } + // Inform the backup manager about a data change + IBackupManager ibm = IBackupManager.Stub.asInterface( + ServiceManager.getService(Context.BACKUP_SERVICE)); + if (ibm != null) { + try { + ibm.dataChanged("com.android.providers.settings"); + } catch (RemoteException e) { + // Try again later + } + } } - + public boolean getSyncProviderAutomatically(String account, String providerName) { synchronized (mAuthorities) { if (account != null) { diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 85d877a..27783ef 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -235,6 +235,12 @@ public class ActivityInfo extends ComponentInfo public static final int CONFIG_ORIENTATION = 0x0080; /** * Bit in {@link #configChanges} that indicates that the activity + * can itself handle changes to the screen layout. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_SCREEN_LAYOUT = 0x0100; + /** + * Bit in {@link #configChanges} that indicates that the activity * can itself handle changes to the font scaling factor. Set from the * {@link android.R.attr#configChanges} attribute. This is * not a core resource configutation, but a higher-level value, so its @@ -248,8 +254,8 @@ public class ActivityInfo extends ComponentInfo * Contains any combination of {@link #CONFIG_FONT_SCALE}, * {@link #CONFIG_MCC}, {@link #CONFIG_MNC}, * {@link #CONFIG_LOCALE}, {@link #CONFIG_TOUCHSCREEN}, - * {@link #CONFIG_KEYBOARD}, {@link #CONFIG_NAVIGATION}, and - * {@link #CONFIG_ORIENTATION}. Set from the + * {@link #CONFIG_KEYBOARD}, {@link #CONFIG_NAVIGATION}, + * {@link #CONFIG_ORIENTATION}, and {@link #CONFIG_SCREEN_LAYOUT}. Set from the * {@link android.R.attr#configChanges} attribute. */ public int configChanges; diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 88ac04c..bcf95b6 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -58,11 +58,22 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * Class implementing the Application's manage space * functionality. From the "manageSpaceActivity" * attribute. This is an optional attribute and will be null if - * application's dont specify it in their manifest + * applications don't specify it in their manifest */ public String manageSpaceActivityName; /** + * Class implementing the Application's backup functionality. From + * the "backupAgent" attribute. This is an optional attribute and + * will be null if the application does not specify it in its manifest. + * + * <p>If android:allowBackup is set to false, this attribute is ignored. + * + * {@hide} + */ + public String backupAgentName; + + /** * Value for {@link #flags}: if set, this application is installed in the * device's system image. */ @@ -93,7 +104,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public static final int FLAG_PERSISTENT = 1<<3; /** - * Value for {@link #flags}: set to true iif this application holds the + * Value for {@link #flags}: set to true if this application holds the * {@link android.Manifest.permission#FACTORY_TEST} permission and the * device is running in factory test mode. */ @@ -123,13 +134,46 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * Value for {@link #flags}: this is set of the application has set * its android:targetSdkVersion to something >= the current SDK version. */ - public static final int FLAG_TARGETS_SDK = 1<<8; + public static final int FLAG_TEST_ONLY = 1<<8; /** - * Value for {@link #flags}: this is set of the application has set - * its android:targetSdkVersion to something >= the current SDK version. + * Value for {@link #flags}: true when the application's window can be + * reduced in size for smaller screens. Corresponds to + * {@link android.R.styleable#AndroidManifestSupportsScreens_smallScreens + * android:smallScreens}. */ - public static final int FLAG_TEST_ONLY = 1<<9; + public static final int FLAG_SUPPORTS_SMALL_SCREENS = 1<<9; + + /** + * Value for {@link #flags}: true when the application's window can be + * displayed on normal screens. Corresponds to + * {@link android.R.styleable#AndroidManifestSupportsScreens_normalScreens + * android:normalScreens}. + */ + public static final int FLAG_SUPPORTS_NORMAL_SCREENS = 1<<10; + + /** + * Value for {@link #flags}: true when the application's window can be + * increased in size for larger screens. Corresponds to + * {@link android.R.styleable#AndroidManifestSupportsScreens_largeScreens + * android:smallScreens}. + */ + public static final int FLAG_SUPPORTS_LARGE_SCREENS = 1<<11; + + /** + * Value for {@link #flags}: this is false if the application has set + * its android:allowBackup to false, true otherwise. + * + * {@hide} + */ + public static final int FLAG_ALLOW_BACKUP = 1<<12; + + /** + * Indicates that the application supports any densities; + * {@hide} + */ + public static final int ANY_DENSITY = -1; + private static final int[] ANY_DENSITIES_ARRAY = { ANY_DENSITY }; /** * Flags associated with the application. Any combination of @@ -137,7 +181,9 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * {@link #FLAG_PERSISTENT}, {@link #FLAG_FACTORY_TEST}, and * {@link #FLAG_ALLOW_TASK_REPARENTING} * {@link #FLAG_ALLOW_CLEAR_USER_DATA}, {@link #FLAG_UPDATED_SYSTEM_APP}, - * {@link #FLAG_TARGETS_SDK}. + * {@link #FLAG_TEST_ONLY}, {@link #FLAG_SUPPORTS_SMALL_SCREENS}, + * {@link #FLAG_SUPPORTS_NORMAL_SCREENS}, + * {@link #FLAG_SUPPORTS_LARGE_SCREENS}. */ public int flags = 0; @@ -173,7 +219,6 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { */ public int uid; - /** * The list of densities in DPI that application supprots. This * field is only set if the {@link PackageManager#GET_SUPPORTS_DENSITIES} flag was @@ -182,6 +227,16 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public int[] supportsDensities; /** + * The minimum SDK version this application targets. It may run on earilier + * versions, but it knows how to work with any new behavior added at this + * version. Will be {@link android.os.Build.VERSION_CODES#CUR_DEVELOPMENT} + * if this is a development build and the app is targeting that. You should + * compare that this number is >= the SDK version number at which your + * behavior was introduced. + */ + public int targetSdkVersion; + + /** * When false, indicates that all components within this application are * considered disabled, regardless of their individually set enabled status. */ @@ -200,6 +255,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { pw.println(prefix + "publicSourceDir=" + publicSourceDir); pw.println(prefix + "sharedLibraryFiles=" + sharedLibraryFiles); pw.println(prefix + "dataDir=" + dataDir); + pw.println(prefix + "targetSdkVersion=" + targetSdkVersion); pw.println(prefix + "enabled=" + enabled); pw.println(prefix + "manageSpaceActivityName="+manageSpaceActivityName); pw.println(prefix + "description=0x"+Integer.toHexString(descriptionRes)); @@ -246,6 +302,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { sharedLibraryFiles = orig.sharedLibraryFiles; dataDir = orig.dataDir; uid = orig.uid; + targetSdkVersion = orig.targetSdkVersion; enabled = orig.enabled; manageSpaceActivityName = orig.manageSpaceActivityName; descriptionRes = orig.descriptionRes; @@ -276,8 +333,10 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeStringArray(sharedLibraryFiles); dest.writeString(dataDir); dest.writeInt(uid); + dest.writeInt(targetSdkVersion); dest.writeInt(enabled ? 1 : 0); dest.writeString(manageSpaceActivityName); + dest.writeString(backupAgentName); dest.writeInt(descriptionRes); dest.writeIntArray(supportsDensities); } @@ -305,8 +364,10 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { sharedLibraryFiles = source.readStringArray(); dataDir = source.readString(); uid = source.readInt(); + targetSdkVersion = source.readInt(); enabled = source.readInt() != 0; manageSpaceActivityName = source.readString(); + backupAgentName = source.readString(); descriptionRes = source.readInt(); supportsDensities = source.createIntArray(); } @@ -331,4 +392,14 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } return null; } + + /** + * Disable compatibility mode + * + * @hide + */ + public void disableCompatibilityMode() { + flags |= FLAG_SUPPORTS_LARGE_SCREENS; + supportsDensities = ANY_DENSITIES_ARRAY; + } } diff --git a/core/java/android/content/pm/ConfigurationInfo.java b/core/java/android/content/pm/ConfigurationInfo.java index dcc7463..fb7a47f 100755 --- a/core/java/android/content/pm/ConfigurationInfo.java +++ b/core/java/android/content/pm/ConfigurationInfo.java @@ -22,7 +22,7 @@ import android.os.Parcelable; /** * Information you can retrieve about hardware configuration preferences * declared by an application. This corresponds to information collected from the - * AndroidManifest.xml's <uses-configuration> tags. + * AndroidManifest.xml's <uses-configuration> and the <uses-feature>tags. */ public class ConfigurationInfo implements Parcelable { /** @@ -70,6 +70,16 @@ public class ConfigurationInfo implements Parcelable { */ public int reqInputFeatures = 0; + /** + * Default value for {@link #reqGlEsVersion}; + */ + public static final int GL_ES_VERSION_UNDEFINED = 0; + /** + * The GLES version used by an application. The upper order 16 bits represent the + * major version and the lower order 16 bits the minor version. + */ + public int reqGlEsVersion; + public ConfigurationInfo() { } @@ -78,6 +88,7 @@ public class ConfigurationInfo implements Parcelable { reqKeyboardType = orig.reqKeyboardType; reqNavigation = orig.reqNavigation; reqInputFeatures = orig.reqInputFeatures; + reqGlEsVersion = orig.reqGlEsVersion; } public String toString() { @@ -86,7 +97,8 @@ public class ConfigurationInfo implements Parcelable { + ", touchscreen = " + reqTouchScreen + "}" + ", inputMethod = " + reqKeyboardType + "}" + ", navigation = " + reqNavigation + "}" - + ", reqInputFeatures = " + reqInputFeatures + "}"; + + ", reqInputFeatures = " + reqInputFeatures + "}" + + ", reqGlEsVersion = " + reqGlEsVersion + "}"; } public int describeContents() { @@ -98,6 +110,7 @@ public class ConfigurationInfo implements Parcelable { dest.writeInt(reqKeyboardType); dest.writeInt(reqNavigation); dest.writeInt(reqInputFeatures); + dest.writeInt(reqGlEsVersion); } public static final Creator<ConfigurationInfo> CREATOR = @@ -115,5 +128,18 @@ public class ConfigurationInfo implements Parcelable { reqKeyboardType = source.readInt(); reqNavigation = source.readInt(); reqInputFeatures = source.readInt(); + reqGlEsVersion = source.readInt(); + } + + /** + * This method extracts the major and minor version of reqGLEsVersion attribute + * and returns it as a string. Say reqGlEsVersion value of 0x00010002 is returned + * as 1.2 + * @return String representation of the reqGlEsVersion attribute + */ + public String getGlEsVersion() { + int major = ((reqGlEsVersion & 0xffff0000) >> 16); + int minor = reqGlEsVersion & 0x0000ffff; + return String.valueOf(major)+"."+String.valueOf(minor); } } diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index c199619..bf2a895 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -34,7 +34,7 @@ import android.content.pm.PermissionInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.net.Uri; -import android.app.PendingIntent; +import android.content.IntentSender; /** * See {@link PackageManager} for documentation on most of the APIs @@ -164,7 +164,12 @@ interface IPackageManager { void addPreferredActivity(in IntentFilter filter, int match, in ComponentName[] set, in ComponentName activity); + + void replacePreferredActivity(in IntentFilter filter, int match, + in ComponentName[] set, in ComponentName activity); + void clearPackagePreferredActivities(String packageName); + int getPreferredActivities(out List<IntentFilter> outFilters, out List<ComponentName> outActivities, String packageName); @@ -229,12 +234,12 @@ interface IPackageManager { * and the current free storage is YY, * if XX is less than YY, just return. if not free XX-YY number * of bytes if possible. - * @param opFinishedIntent PendingIntent call back used to + * @param pi IntentSender call back used to * notify when the operation is completed.May be null * to indicate that no call back is desired. */ void freeStorage(in long freeStorageSize, - in PendingIntent opFinishedIntent); + in IntentSender pi); /** * Delete all the cache files in an applications cache directory @@ -271,4 +276,11 @@ interface IPackageManager { boolean isSafeMode(); void systemReady(); boolean hasSystemUidErrors(); + + /** + * Ask the package manager to perform dex-opt (if needed) on the given + * package, if it already hasn't done mode. Only does this if running + * in the special development "no pre-dexopt" mode. + */ + boolean performDexOpt(String packageName); } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 3a192f7..941ca9e 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -16,12 +16,11 @@ package android.content.pm; - -import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentSender; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.drawable.Drawable; @@ -398,6 +397,15 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_TEST_ONLY = -15; /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the package being installed contains native code, but none that is + * compatible with the the device's CPU_ABI. + * @hide + */ + public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16; + + /** * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} * if the parser was given a path that is not a file, or does not end with the expected @@ -563,9 +571,8 @@ public abstract class PackageManager { * launch the main activity in the package, or null if the package does * not contain such an activity. */ - public abstract Intent getLaunchIntentForPackage(String packageName) - throws NameNotFoundException; - + public abstract Intent getLaunchIntentForPackage(String packageName); + /** * Return an array of all of the secondary group-ids that have been * assigned to a package. @@ -1491,7 +1498,7 @@ public abstract class PackageManager { * @hide */ public abstract void freeStorageAndNotify(long freeStorageSize, IPackageDataObserver observer); - + /** * Free storage by deleting LRU sorted list of cache files across * all applications. If the currently available free storage @@ -1509,13 +1516,13 @@ public abstract class PackageManager { * and the current free storage is YY, * if XX is less than YY, just return. if not free XX-YY number * of bytes if possible. - * @param opFinishedIntent PendingIntent call back used to + * @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, PendingIntent opFinishedIntent); + public abstract void freeStorage(long freeStorageSize, IntentSender pi); /** * Retrieve the size information for a package. @@ -1605,6 +1612,26 @@ public abstract class PackageManager { ComponentName[] set, ComponentName activity); /** + * 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 + * {@link Context#startActivity(Intent) Context.startActivity()} finds + * multiple matching activities and also matches the given filter. + * + * @param filter The set of intents under which this activity will be + * made preferred. + * @param match The IntentFilter match category that this preference + * applies to. + * @param set The set of activities that the user was picking from when + * this preference was made. + * @param activity The component name of the activity that is to be + * preferred. + * @hide + */ + public abstract void replacePreferredActivity(IntentFilter filter, int match, + ComponentName[] set, ComponentName activity); + + /** * Remove all preferred activity mappings, previously added with * {@link #addPreferredActivity}, from the * system whose activities are implemented in the given package name. diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 88907c1..558b0c3 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -55,6 +55,32 @@ import java.util.jar.JarFile; * {@hide} */ public class PackageParser { + /** @hide */ + public static class NewPermissionInfo { + public final String name; + public final int sdkVersion; + public final int fileVersion; + + public NewPermissionInfo(String name, int sdkVersion, int fileVersion) { + this.name = name; + this.sdkVersion = sdkVersion; + this.fileVersion = fileVersion; + } + } + + /** + * List of new permissions that have been added since 1.0. + * NOTE: These must be declared in SDK version order, with permissions + * added to older SDKs appearing before those added to newer SDKs. + * @hide + */ + public static final PackageParser.NewPermissionInfo NEW_PERMISSIONS[] = + new PackageParser.NewPermissionInfo[] { + new PackageParser.NewPermissionInfo(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + android.os.Build.VERSION_CODES.DONUT, 0), + new PackageParser.NewPermissionInfo(android.Manifest.permission.READ_PHONE_STATE, + android.os.Build.VERSION_CODES.DONUT, 0) + }; private String mArchiveSourcePath; private String[] mSeparateProcesses; @@ -616,7 +642,6 @@ public class PackageParser { final Package pkg = new Package(pkgName); boolean foundApp = false; - boolean targetsSdk = false; TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifest); @@ -643,6 +668,11 @@ public class PackageParser { } sa.recycle(); + // Resource boolean are -1, so 1 means we don't know the value. + int supportsSmallScreens = 1; + int supportsNormalScreens = 1; + int supportsLargeScreens = 1; + int outerDepth = parser.getDepth(); while ((type=parser.next()) != parser.END_DOCUMENT && (type != parser.END_TAG || parser.getDepth() > outerDepth)) { @@ -723,6 +753,18 @@ public class PackageParser { XmlUtils.skipCurrentTag(parser); + } else if (tagName.equals("uses-feature")) { + ConfigurationInfo cPref = new ConfigurationInfo(); + sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestUsesFeature); + cPref.reqGlEsVersion = sa.getInt( + com.android.internal.R.styleable.AndroidManifestUsesFeature_glEsVersion, + ConfigurationInfo.GL_ES_VERSION_UNDEFINED); + sa.recycle(); + pkg.configPreferences.add(cPref); + + XmlUtils.skipCurrentTag(parser); + } else if (tagName.equals("uses-sdk")) { if (mSdkVersion > 0) { sa = res.obtainAttributes(attrs, @@ -740,7 +782,7 @@ public class PackageParser { targetCode = minCode = val.string.toString(); } else { // If it's not a string, it's an integer. - minVers = val.data; + targetVers = minVers = val.data; } } @@ -761,6 +803,25 @@ public class PackageParser { sa.recycle(); + if (minCode != null) { + if (!minCode.equals(mSdkCodename)) { + if (mSdkCodename != null) { + outError[0] = "Requires development platform " + minCode + + " (current platform is " + mSdkCodename + ")"; + } else { + outError[0] = "Requires development platform " + minCode + + " but this is a release platform."; + } + mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; + return null; + } + } else if (minVers > mSdkVersion) { + outError[0] = "Requires newer sdk version #" + minVers + + " (current version is #" + mSdkVersion + ")"; + mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; + return null; + } + if (targetCode != null) { if (!targetCode.equals(mSdkCodename)) { if (mSdkCodename != null) { @@ -774,18 +835,10 @@ public class PackageParser { return null; } // If the code matches, it definitely targets this SDK. - targetsSdk = true; - } else if (targetVers >= mSdkVersion) { - // If they have explicitly targeted our current version - // or something after it, then note this. - targetsSdk = true; - } - - if (minVers > mSdkVersion) { - outError[0] = "Requires newer sdk version #" + minVers - + " (current version is #" + mSdkVersion + ")"; - mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; - return null; + pkg.applicationInfo.targetSdkVersion + = android.os.Build.VERSION_CODES.CUR_DEVELOPMENT; + } else { + pkg.applicationInfo.targetSdkVersion = targetVers; } if (maxVers < mSdkVersion) { @@ -811,6 +864,42 @@ public class PackageParser { + parser.getName(); mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return null; + + + } else if (tagName.equals("supports-density")) { + sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestSupportsDensity); + + int density = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsDensity_density, -1); + + sa.recycle(); + + if (density != -1 && !pkg.supportsDensityList.contains(density)) { + pkg.supportsDensityList.add(density); + } + + XmlUtils.skipCurrentTag(parser); + + } else if (tagName.equals("supports-screens")) { + sa = res.obtainAttributes(attrs, + com.android.internal.R.styleable.AndroidManifestSupportsScreens); + + // This is a trick to get a boolean and still able to detect + // if a value was actually set. + supportsSmallScreens = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_smallScreens, + supportsSmallScreens); + supportsNormalScreens = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_normalScreens, + supportsNormalScreens); + supportsLargeScreens = sa.getInteger( + com.android.internal.R.styleable.AndroidManifestSupportsScreens_largeScreens, + supportsLargeScreens); + + sa.recycle(); + + XmlUtils.skipCurrentTag(parser); } else { Log.w(TAG, "Bad element under <manifest>: " + parser.getName()); @@ -824,15 +913,39 @@ public class PackageParser { mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY; } - if (targetsSdk) { - pkg.applicationInfo.flags |= ApplicationInfo.FLAG_TARGETS_SDK; + final int NP = PackageParser.NEW_PERMISSIONS.length; + for (int ip=0; ip<NP; ip++) { + final PackageParser.NewPermissionInfo npi + = PackageParser.NEW_PERMISSIONS[ip]; + if (pkg.applicationInfo.targetSdkVersion >= npi.sdkVersion) { + break; + } + if (!pkg.requestedPermissions.contains(npi.name)) { + Log.i(TAG, "Impliciting adding " + npi.name + " to old pkg " + + pkg.packageName); + pkg.requestedPermissions.add(npi.name); + } } if (pkg.usesLibraries.size() > 0) { pkg.usesLibraryFiles = new String[pkg.usesLibraries.size()]; pkg.usesLibraries.toArray(pkg.usesLibraryFiles); } - + + if (supportsSmallScreens < 0 || (supportsSmallScreens > 0 + && pkg.applicationInfo.targetSdkVersion + >= android.os.Build.VERSION_CODES.CUR_DEVELOPMENT)) { + pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS; + } + if (supportsNormalScreens != 0) { + pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_NORMAL_SCREENS; + } + if (supportsLargeScreens < 0 || (supportsLargeScreens > 0 + && pkg.applicationInfo.targetSdkVersion + >= android.os.Build.VERSION_CODES.CUR_DEVELOPMENT)) { + pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS; + } + int size = pkg.supportsDensityList.size(); if (size > 0) { int densities[] = pkg.supportsDensities = new int[size]; @@ -1142,6 +1255,19 @@ public class PackageParser { outError); } + boolean allowBackup = sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_allowBackup, true); + if (allowBackup) { + ai.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP; + String backupAgent = sa.getNonResourceString( + com.android.internal.R.styleable.AndroidManifestApplication_backupAgent); + if (backupAgent != null) { + ai.backupAgentName = buildClassName(pkgName, backupAgent, outError); + Log.v(TAG, "android:backupAgent = " + ai.backupAgentName + + " from " + pkgName + "+" + backupAgent); + } + } + TypedValue v = sa.peekValue( com.android.internal.R.styleable.AndroidManifestApplication_label); if (v != null && (ai.labelRes=v.resourceId) == 0) { @@ -1298,21 +1424,6 @@ public class PackageParser { XmlUtils.skipCurrentTag(parser); - } else if (tagName.equals("supports-density")) { - sa = res.obtainAttributes(attrs, - com.android.internal.R.styleable.AndroidManifestSupportsDensity); - - int density = sa.getInteger( - com.android.internal.R.styleable.AndroidManifestSupportsDensity_density, -1); - - sa.recycle(); - - if (density != -1 && !owner.supportsDensityList.contains(density)) { - owner.supportsDensityList.add(density); - } - - XmlUtils.skipCurrentTag(parser); - } else { if (!RIGID_PARSER) { Log.w(TAG, "Problem in package " + mArchiveSourcePath + ":"); @@ -2219,6 +2330,17 @@ public class PackageParser { // preferred up order. public int mPreferredOrder = 0; + // For use by package manager service to keep track of which apps + // have been installed with forward locking. + public boolean mForwardLocked; + + // For use by the package manager to keep track of the path to the + // file an app came from. + public String mScanPath; + + // For use by package manager to keep track of where it has done dexopt. + public boolean mDidDexOpt; + // Additional data supplied by callers. public Object mExtras; @@ -2368,7 +2490,7 @@ public class PackageParser { return true; } if ((flags & PackageManager.GET_SUPPORTS_DENSITIES) != 0 - && p.supportsDensities != null) { + && p.supportsDensities != null) { return true; } return false; diff --git a/core/java/android/content/res/AssetFileDescriptor.java b/core/java/android/content/res/AssetFileDescriptor.java index 231e3e2..a37e4e8 100644 --- a/core/java/android/content/res/AssetFileDescriptor.java +++ b/core/java/android/content/res/AssetFileDescriptor.java @@ -16,6 +16,7 @@ package android.content.res; +import android.os.MemoryFile; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; @@ -24,6 +25,8 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.FileChannel; /** * File descriptor of an entry in the AssetManager. This provides your own @@ -124,6 +127,13 @@ public class AssetFileDescriptor implements Parcelable { } /** + * Checks whether this file descriptor is for a memory file. + */ + private boolean isMemoryFile() throws IOException { + return MemoryFile.isMemoryFile(mFd.getFileDescriptor()); + } + + /** * Create and return a new auto-close input stream for this asset. This * will either return a full asset {@link AutoCloseInputStream}, or * an underlying {@link ParcelFileDescriptor.AutoCloseInputStream @@ -132,6 +142,12 @@ public class AssetFileDescriptor implements Parcelable { * should only call this once for a particular asset. */ public FileInputStream createInputStream() throws IOException { + if (isMemoryFile()) { + if (mLength > Integer.MAX_VALUE) { + throw new IOException("File length too large for a memory file: " + mLength); + } + return new AutoCloseMemoryFileInputStream(mFd, (int)mLength); + } if (mLength < 0) { return new ParcelFileDescriptor.AutoCloseInputStream(mFd); } @@ -262,6 +278,66 @@ public class AssetFileDescriptor implements Parcelable { } /** + * An input stream that reads from a MemoryFile and closes it when the stream is closed. + * This extends FileInputStream just because {@link #createInputStream} returns + * a FileInputStream. All the FileInputStream methods are + * overridden to use the MemoryFile instead. + */ + private static class AutoCloseMemoryFileInputStream extends FileInputStream { + private ParcelFileDescriptor mParcelFd; + private MemoryFile mFile; + private InputStream mStream; + + public AutoCloseMemoryFileInputStream(ParcelFileDescriptor fd, int length) + throws IOException { + super(fd.getFileDescriptor()); + mParcelFd = fd; + mFile = new MemoryFile(fd.getFileDescriptor(), length, "r"); + mStream = mFile.getInputStream(); + } + + @Override + public int available() throws IOException { + return mStream.available(); + } + + @Override + public void close() throws IOException { + mParcelFd.close(); // must close ParcelFileDescriptor, not just the file descriptor, + // since it could be a subclass of ParcelFileDescriptor. + // E.g. ContentResolver.ParcelFileDescriptorInner.close() releases + // a content provider + mFile.close(); // to unmap the memory file from the address space. + mStream.close(); // doesn't actually do anything + } + + @Override + public FileChannel getChannel() { + return null; + } + + @Override + public int read() throws IOException { + return mStream.read(); + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + return mStream.read(buffer, offset, count); + } + + @Override + public int read(byte[] buffer) throws IOException { + return mStream.read(buffer); + } + + @Override + public long skip(long count) throws IOException { + return mStream.skip(count); + } + } + + /** * An OutputStream you can create on a ParcelFileDescriptor, which will * take care of calling {@link ParcelFileDescriptor#close * ParcelFileDescritor.close()} for you when the stream is closed. @@ -345,4 +421,16 @@ public class AssetFileDescriptor implements Parcelable { return new AssetFileDescriptor[size]; } }; + + /** + * Creates an AssetFileDescriptor from a memory file. + * + * @hide + */ + public static AssetFileDescriptor fromMemoryFile(MemoryFile memoryFile) + throws IOException { + ParcelFileDescriptor fd = memoryFile.getParcelFileDescriptor(); + return new AssetFileDescriptor(fd, 0, memoryFile.length()); + } + } diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index 1c91736..5c7b01f 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -601,7 +601,7 @@ public final class AssetManager { public native final void setConfiguration(int mcc, int mnc, String locale, int orientation, int touchscreen, int density, int keyboard, int keyboardHidden, int navigation, int screenWidth, int screenHeight, - int majorVersion); + int screenLayout, int majorVersion); /** * Retrieve the resource identifier for the given resource name. diff --git a/core/java/android/content/res/CompatibilityInfo.java b/core/java/android/content/res/CompatibilityInfo.java new file mode 100644 index 0000000..dfe304d --- /dev/null +++ b/core/java/android/content/res/CompatibilityInfo.java @@ -0,0 +1,491 @@ +/* + * 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.content.res; + +import android.content.pm.ApplicationInfo; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.Region; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.WindowManager; +import android.view.WindowManager.LayoutParams; + +/** + * CompatibilityInfo class keeps the information about compatibility mode that the application is + * running under. + * + * {@hide} + */ +public class CompatibilityInfo { + private static final boolean DBG = false; + private static final String TAG = "CompatibilityInfo"; + + /** default compatibility info object for compatible applications */ + public static final CompatibilityInfo DEFAULT_COMPATIBILITY_INFO = new CompatibilityInfo(); + + /** + * The default width of the screen in portrait mode. + */ + public static final int DEFAULT_PORTRAIT_WIDTH = 320; + + /** + * The default height of the screen in portrait mode. + */ + public static final int DEFAULT_PORTRAIT_HEIGHT = 480; + + /** + * The x-shift mode that controls the position of the content or the window under + * compatibility mode. + * {@see getTranslator} + * {@see Translator#mShiftMode} + */ + private static final int X_SHIFT_NONE = 0; + private static final int X_SHIFT_CONTENT = 1; + private static final int X_SHIFT_AND_CLIP_CONTENT = 2; + private static final int X_SHIFT_WINDOW = 3; + + + /** + * A compatibility flags + */ + private int mCompatibilityFlags; + + /** + * A flag mask to tell if the application needs scaling (when mApplicationScale != 1.0f) + * {@see compatibilityFlag} + */ + private static final int SCALING_REQUIRED = 1; + + /** + * A flag mask to indicates that the application can expand over the original size. + * The flag is set to true if + * 1) Application declares its expandable in manifest file using <expandable /> or + * 2) The screen size is same as (320 x 480) * density. + * {@see compatibilityFlag} + */ + private static final int EXPANDABLE = 2; + + /** + * A flag mask to tell if the application is configured to be expandable. This differs + * from EXPANDABLE in that the application that is not expandable will be + * marked as expandable if it runs in (320x 480) * density screen size. + */ + private static final int CONFIGURED_EXPANDABLE = 4; + + private static final int SCALING_EXPANDABLE_MASK = SCALING_REQUIRED | EXPANDABLE; + + /** + * Application's scale. + */ + public final float applicationScale; + + /** + * Application's inverted scale. + */ + public final float applicationInvertedScale; + + /** + * The flags from ApplicationInfo. + */ + public final int appFlags; + + /** + * Window size in Compatibility Mode, in real pixels. This is updated by + * {@link DisplayMetrics#updateMetrics}. + */ + private int mWidth; + private int mHeight; + + /** + * The x offset to center the window content. In X_SHIFT_WINDOW mode, the offset is added + * to the window's layout. In X_SHIFT_CONTENT/X_SHIFT_AND_CLIP_CONTENT mode, the offset + * is used to translate the Canvas. + */ + private int mXOffset; + + public CompatibilityInfo(ApplicationInfo appInfo) { + appFlags = appInfo.flags; + + if ((appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS) != 0) { + mCompatibilityFlags = EXPANDABLE | CONFIGURED_EXPANDABLE; + } + + float packageDensityScale = -1.0f; + if (appInfo.supportsDensities != null) { + int minDiff = Integer.MAX_VALUE; + for (int density : appInfo.supportsDensities) { + if (density == ApplicationInfo.ANY_DENSITY) { + packageDensityScale = 1.0f; + break; + } + int tmpDiff = Math.abs(DisplayMetrics.DEVICE_DENSITY - density); + if (tmpDiff == 0) { + packageDensityScale = 1.0f; + break; + } + // prefer higher density (appScale>1.0), unless that's only option. + if (tmpDiff < minDiff && packageDensityScale < 1.0f) { + packageDensityScale = DisplayMetrics.DEVICE_DENSITY / (float) density; + minDiff = tmpDiff; + } + } + } + if (packageDensityScale > 0.0f) { + applicationScale = packageDensityScale; + } else { + applicationScale = + DisplayMetrics.DEVICE_DENSITY / (float) DisplayMetrics.DEFAULT_DENSITY; + } + applicationInvertedScale = 1.0f / applicationScale; + if (applicationScale != 1.0f) { + mCompatibilityFlags |= SCALING_REQUIRED; + } + } + + private CompatibilityInfo(int appFlags, int compFlags, float scale, float invertedScale) { + this.appFlags = appFlags; + mCompatibilityFlags = compFlags; + applicationScale = scale; + applicationInvertedScale = invertedScale; + } + + private CompatibilityInfo() { + this(ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS + | ApplicationInfo.FLAG_SUPPORTS_NORMAL_SCREENS + | ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS, + EXPANDABLE | CONFIGURED_EXPANDABLE, + 1.0f, + 1.0f); + } + + /** + * Returns the copy of this instance. + */ + public CompatibilityInfo copy() { + CompatibilityInfo info = new CompatibilityInfo(appFlags, mCompatibilityFlags, + applicationScale, applicationInvertedScale); + info.setVisibleRect(mXOffset, mWidth, mHeight); + return info; + } + + /** + * Sets the application's visible rect in compatibility mode. + * @param xOffset the application's x offset that is added to center the content. + * @param widthPixels the application's width in real pixels on the screen. + * @param heightPixels the application's height in real pixels on the screen. + */ + public void setVisibleRect(int xOffset, int widthPixels, int heightPixels) { + this.mXOffset = xOffset; + mWidth = widthPixels; + mHeight = heightPixels; + } + + /** + * Sets expandable bit in the compatibility flag. + */ + public void setExpandable(boolean expandable) { + if (expandable) { + mCompatibilityFlags |= CompatibilityInfo.EXPANDABLE; + } else { + mCompatibilityFlags &= ~CompatibilityInfo.EXPANDABLE; + } + } + + /** + * @return true if the application is configured to be expandable. + */ + public boolean isConfiguredExpandable() { + return (mCompatibilityFlags & CompatibilityInfo.CONFIGURED_EXPANDABLE) != 0; + } + + /** + * @return true if the scaling is required + */ + public boolean isScalingRequired() { + return (mCompatibilityFlags & SCALING_REQUIRED) != 0; + } + + @Override + public String toString() { + return "CompatibilityInfo{scale=" + applicationScale + + ", compatibility flag=" + mCompatibilityFlags + "}"; + } + + /** + * Returns the translator which can translate the coordinates of the window. + * There are five different types of Translator. + * + * 1) {@link CompatibilityInfo#X_SHIFT_AND_CLIP_CONTENT} + * Shift and clip the content of the window at drawing time. Used for activities' + * main window (with no gravity). + * 2) {@link CompatibilityInfo#X_SHIFT_CONTENT} + * Shift the content of the window at drawing time. Used for windows that is created by + * an application and expected to be aligned with the application window. + * 3) {@link CompatibilityInfo#X_SHIFT_WINDOW} + * Create the window with adjusted x- coordinates. This is typically used + * in popup window, where it has to be placed relative to main window. + * 4) {@link CompatibilityInfo#X_SHIFT_NONE} + * No adjustment required, such as dialog. + * 5) Same as X_SHIFT_WINDOW, but no scaling. This is used by {@link SurfaceView}, which + * does not require scaling, but its window's location has to be adjusted. + * + * @param params the window's parameter + */ + public Translator getTranslator(WindowManager.LayoutParams params) { + if ( (mCompatibilityFlags & CompatibilityInfo.SCALING_EXPANDABLE_MASK) + == CompatibilityInfo.EXPANDABLE) { + if (DBG) Log.d(TAG, "no translation required"); + return null; + } + + if ((mCompatibilityFlags & CompatibilityInfo.EXPANDABLE) == 0) { + if ((params.flags & WindowManager.LayoutParams.FLAG_NO_COMPATIBILITY_SCALING) != 0) { + if (DBG) Log.d(TAG, "translation for surface view selected"); + return new Translator(X_SHIFT_WINDOW, false, 1.0f, 1.0f); + } else { + int shiftMode; + if (params.gravity == Gravity.NO_GRAVITY) { + // For Regular Application window + shiftMode = X_SHIFT_AND_CLIP_CONTENT; + if (DBG) Log.d(TAG, "shift and clip translator"); + } else if (params.width == WindowManager.LayoutParams.FILL_PARENT) { + // For Regular Application window + shiftMode = X_SHIFT_CONTENT; + if (DBG) Log.d(TAG, "shift content translator"); + } else if ((params.gravity & Gravity.LEFT) != 0 && params.x > 0) { + shiftMode = X_SHIFT_WINDOW; + if (DBG) Log.d(TAG, "shift window translator"); + } else { + shiftMode = X_SHIFT_NONE; + if (DBG) Log.d(TAG, "no content/window translator"); + } + return new Translator(shiftMode); + } + } else if (isScalingRequired()) { + return new Translator(); + } else { + return null; + } + } + + /** + * A helper object to translate the screen and window coordinates back and forth. + * @hide + */ + public class Translator { + final private int mShiftMode; + final public boolean scalingRequired; + final public float applicationScale; + final public float applicationInvertedScale; + + private Rect mContentInsetsBuffer = null; + private Rect mVisibleInsets = null; + + Translator(int shiftMode, boolean scalingRequired, float applicationScale, + float applicationInvertedScale) { + mShiftMode = shiftMode; + this.scalingRequired = scalingRequired; + this.applicationScale = applicationScale; + this.applicationInvertedScale = applicationInvertedScale; + } + + Translator(int shiftMode) { + this(shiftMode, + isScalingRequired(), + CompatibilityInfo.this.applicationScale, + CompatibilityInfo.this.applicationInvertedScale); + } + + Translator() { + this(X_SHIFT_NONE); + } + + /** + * Translate the screen rect to the application frame. + */ + public void translateRectInScreenToAppWinFrame(Rect rect) { + if (rect.isEmpty()) return; // skip if the window size is empty. + switch (mShiftMode) { + case X_SHIFT_AND_CLIP_CONTENT: + rect.intersect(0, 0, mWidth, mHeight); + break; + case X_SHIFT_CONTENT: + rect.intersect(0, 0, mWidth + mXOffset, mHeight); + break; + case X_SHIFT_WINDOW: + case X_SHIFT_NONE: + break; + } + if (scalingRequired) { + rect.scale(applicationInvertedScale); + } + } + + /** + * Translate the region in window to screen. + */ + public void translateRegionInWindowToScreen(Region transparentRegion) { + switch (mShiftMode) { + case X_SHIFT_AND_CLIP_CONTENT: + case X_SHIFT_CONTENT: + transparentRegion.scale(applicationScale); + transparentRegion.translate(mXOffset, 0); + break; + case X_SHIFT_WINDOW: + case X_SHIFT_NONE: + transparentRegion.scale(applicationScale); + } + } + + /** + * Apply translation to the canvas that is necessary to draw the content. + */ + public void translateCanvas(Canvas canvas) { + if (mShiftMode == X_SHIFT_CONTENT || + mShiftMode == X_SHIFT_AND_CLIP_CONTENT) { + // TODO: clear outside when rotation is changed. + + // Translate x-offset only when the content is shifted. + canvas.translate(mXOffset, 0); + } + if (scalingRequired) { + canvas.scale(applicationScale, applicationScale); + } + } + + /** + * Translate the motion event captured on screen to the application's window. + */ + public void translateEventInScreenToAppWindow(MotionEvent event) { + if (mShiftMode == X_SHIFT_CONTENT || + mShiftMode == X_SHIFT_AND_CLIP_CONTENT) { + event.translate(-mXOffset, 0); + } + if (scalingRequired) { + event.scale(applicationInvertedScale); + } + } + + /** + * Translate the window's layout parameter, from application's view to + * Screen's view. + */ + public void translateWindowLayout(WindowManager.LayoutParams params) { + switch (mShiftMode) { + case X_SHIFT_NONE: + case X_SHIFT_AND_CLIP_CONTENT: + case X_SHIFT_CONTENT: + params.scale(applicationScale); + break; + case X_SHIFT_WINDOW: + params.scale(applicationScale); + params.x += mXOffset; + break; + } + } + + /** + * Translate a Rect in application's window to screen. + */ + public void translateRectInAppWindowToScreen(Rect rect) { + // TODO Auto-generated method stub + if (scalingRequired) { + rect.scale(applicationScale); + } + switch(mShiftMode) { + case X_SHIFT_NONE: + case X_SHIFT_WINDOW: + break; + case X_SHIFT_CONTENT: + case X_SHIFT_AND_CLIP_CONTENT: + rect.offset(mXOffset, 0); + break; + } + } + + /** + * Translate a Rect in screen coordinates into the app window's coordinates. + */ + public void translateRectInScreenToAppWindow(Rect rect) { + switch (mShiftMode) { + case X_SHIFT_NONE: + case X_SHIFT_WINDOW: + break; + case X_SHIFT_CONTENT: { + rect.intersects(mXOffset, 0, rect.right, rect.bottom); + int dx = Math.min(mXOffset, rect.left); + rect.offset(-dx, 0); + break; + } + case X_SHIFT_AND_CLIP_CONTENT: { + rect.intersects(mXOffset, 0, mWidth + mXOffset, mHeight); + int dx = Math.min(mXOffset, rect.left); + rect.offset(-dx, 0); + break; + } + } + if (scalingRequired) { + rect.scale(applicationInvertedScale); + } + } + + /** + * Translate the location of the sub window. + * @param params + */ + public void translateLayoutParamsInAppWindowToScreen(LayoutParams params) { + if (scalingRequired) { + params.scale(applicationScale); + } + switch (mShiftMode) { + // the window location on these mode does not require adjustmenet. + case X_SHIFT_NONE: + case X_SHIFT_WINDOW: + break; + case X_SHIFT_CONTENT: + case X_SHIFT_AND_CLIP_CONTENT: + params.x += mXOffset; + break; + } + } + + /** + * Translate the content insets in application window to Screen. This uses + * the internal buffer for content insets to avoid extra object allocation. + */ + public Rect getTranslatedContentInsets(Rect contentInsets) { + if (mContentInsetsBuffer == null) mContentInsetsBuffer = new Rect(); + mContentInsetsBuffer.set(contentInsets); + translateRectInAppWindowToScreen(mContentInsetsBuffer); + return mContentInsetsBuffer; + } + + /** + * Translate the visible insets in application window to Screen. This uses + * the internal buffer for content insets to avoid extra object allocation. + */ + public Rect getTranslatedVisbileInsets(Rect visibleInsets) { + if (mVisibleInsets == null) mVisibleInsets = new Rect(); + mVisibleInsets.set(visibleInsets); + translateRectInAppWindowToScreen(mVisibleInsets); + return mVisibleInsets; + } + } +} diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index bb3486c..577aa60 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -116,6 +116,18 @@ public final class Configuration implements Parcelable, Comparable<Configuration */ public int orientation; + public static final int SCREENLAYOUT_UNDEFINED = 0; + public static final int SCREENLAYOUT_SMALL = 1; + public static final int SCREENLAYOUT_NORMAL = 2; + public static final int SCREENLAYOUT_LARGE = 3; + + /** + * Overall layout of the screen. May be one of + * {@link #SCREENLAYOUT_SMALL}, {@link #SCREENLAYOUT_NORMAL}, + * or {@link #SCREENLAYOUT_LARGE}. + */ + public int screenLayout; + /** * Construct an invalid Configuration. You must call {@link #setToDefaults} * for this object to be valid. {@more} @@ -141,6 +153,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration hardKeyboardHidden = o.hardKeyboardHidden; navigation = o.navigation; orientation = o.orientation; + screenLayout = o.screenLayout; } public String toString() { @@ -165,6 +178,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration sb.append(navigation); sb.append(" orien="); sb.append(orientation); + sb.append(" layout="); + sb.append(screenLayout); sb.append('}'); return sb.toString(); } @@ -183,6 +198,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration hardKeyboardHidden = HARDKEYBOARDHIDDEN_UNDEFINED; navigation = NAVIGATION_UNDEFINED; orientation = ORIENTATION_UNDEFINED; + screenLayout = SCREENLAYOUT_UNDEFINED; } /** {@hide} */ @@ -253,6 +269,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration changed |= ActivityInfo.CONFIG_ORIENTATION; orientation = delta.orientation; } + if (delta.screenLayout != SCREENLAYOUT_UNDEFINED + && screenLayout != delta.screenLayout) { + changed |= ActivityInfo.CONFIG_SCREEN_LAYOUT; + screenLayout = delta.screenLayout; + } return changed; } @@ -276,9 +297,11 @@ public final class Configuration implements Parcelable, Comparable<Configuration * {@link android.content.pm.ActivityInfo#CONFIG_KEYBOARD * PackageManager.ActivityInfo.CONFIG_KEYBOARD}, * {@link android.content.pm.ActivityInfo#CONFIG_NAVIGATION - * PackageManager.ActivityInfo.CONFIG_NAVIGATION}, or + * PackageManager.ActivityInfo.CONFIG_NAVIGATION}, * {@link android.content.pm.ActivityInfo#CONFIG_ORIENTATION - * PackageManager.ActivityInfo.CONFIG_ORIENTATION}. + * PackageManager.ActivityInfo.CONFIG_ORIENTATION}, or + * {@link android.content.pm.ActivityInfo#CONFIG_SCREEN_LAYOUT + * PackageManager.ActivityInfo.CONFIG_SCREEN_LAYOUT}. */ public int diff(Configuration delta) { int changed = 0; @@ -319,6 +342,10 @@ public final class Configuration implements Parcelable, Comparable<Configuration && orientation != delta.orientation) { changed |= ActivityInfo.CONFIG_ORIENTATION; } + if (delta.screenLayout != SCREENLAYOUT_UNDEFINED + && screenLayout != delta.screenLayout) { + changed |= ActivityInfo.CONFIG_SCREEN_LAYOUT; + } return changed; } @@ -368,6 +395,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration dest.writeInt(hardKeyboardHidden); dest.writeInt(navigation); dest.writeInt(orientation); + dest.writeInt(screenLayout); } public static final Parcelable.Creator<Configuration> CREATOR @@ -399,6 +427,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration hardKeyboardHidden = source.readInt(); navigation = source.readInt(); orientation = source.readInt(); + screenLayout = source.readInt(); } public int compareTo(Configuration that) { @@ -428,6 +457,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration n = this.navigation - that.navigation; if (n != 0) return n; n = this.orientation - that.orientation; + if (n != 0) return n; + n = this.screenLayout - that.screenLayout; //if (n != 0) return n; return n; } @@ -450,6 +481,6 @@ public final class Configuration implements Parcelable, Comparable<Configuration return ((int)this.fontScale) + this.mcc + this.mnc + this.locale.hashCode() + this.touchscreen + this.keyboard + this.keyboardHidden + this.hardKeyboardHidden - + this.navigation + this.orientation; + + this.navigation + this.orientation + this.screenLayout; } } diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 665e40c..49ad656 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -22,9 +22,9 @@ import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.content.pm.ApplicationInfo; import android.graphics.Movie; import android.graphics.drawable.Drawable; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.os.SystemProperties; @@ -33,6 +33,7 @@ import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; import android.util.TypedValue; +import android.util.LongSparseArray; import java.io.IOException; import java.io.InputStream; @@ -57,19 +58,19 @@ public class Resources { // Information about preloaded resources. Note that they are not // protected by a lock, because while preloading in zygote we are all // single-threaded, and after that these are immutable. - private static final SparseArray<Drawable.ConstantState> sPreloadedDrawables - = new SparseArray<Drawable.ConstantState>(); + private static final LongSparseArray<Drawable.ConstantState> sPreloadedDrawables + = new LongSparseArray<Drawable.ConstantState>(); private static final SparseArray<ColorStateList> mPreloadedColorStateLists = new SparseArray<ColorStateList>(); private static boolean mPreloaded; - private final SparseArray<Drawable.ConstantState> mPreloadedDrawables; + private final LongSparseArray<Drawable.ConstantState> mPreloadedDrawables; /*package*/ final TypedValue mTmpValue = new TypedValue(); // These are protected by the mTmpValue lock. - private final SparseArray<WeakReference<Drawable.ConstantState> > mDrawableCache - = new SparseArray<WeakReference<Drawable.ConstantState> >(); + private final LongSparseArray<WeakReference<Drawable.ConstantState> > mDrawableCache + = new LongSparseArray<WeakReference<Drawable.ConstantState> >(); private final SparseArray<WeakReference<ColorStateList> > mColorStateListCache = new SparseArray<WeakReference<ColorStateList> >(); private boolean mPreloading; @@ -84,21 +85,23 @@ public class Resources { private final Configuration mConfiguration = new Configuration(); /*package*/ final DisplayMetrics mMetrics = new DisplayMetrics(); PluralRules mPluralRule; + + private final CompatibilityInfo mCompatibilityInfo; - private static final SparseArray<Object> EMPTY_ARRAY = new SparseArray<Object>() { + private static final LongSparseArray<Object> EMPTY_ARRAY = new LongSparseArray<Object>() { @Override - public void put(int k, Object o) { + public void put(long k, Object o) { throw new UnsupportedOperationException(); } @Override - public void append(int k, Object o) { + public void append(long k, Object o) { throw new UnsupportedOperationException(); } }; @SuppressWarnings("unchecked") - private static <T> SparseArray<T> emptySparseArray() { - return (SparseArray<T>) EMPTY_ARRAY; + private static <T> LongSparseArray<T> emptySparseArray() { + return (LongSparseArray<T>) EMPTY_ARRAY; } /** @@ -126,26 +129,59 @@ public class Resources { */ public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) { - this(assets, metrics, config, true); + this(assets, metrics, config, (ApplicationInfo) null); } /** - * Create a resource with an additional flag for preloaded - * drawable cache. Used by {@link ActivityThread}. - * + * Creates a new Resources object with ApplicationInfo. + * + * @param assets Previously created AssetManager. + * @param metrics Current display metrics to consider when + * selecting/computing resource values. + * @param config Desired device configuration to consider when + * selecting/computing resource values (optional). + * @param appInfo this resource's application info. * @hide */ public Resources(AssetManager assets, DisplayMetrics metrics, - Configuration config, boolean usePreloadedCache) { + Configuration config, ApplicationInfo appInfo) { mAssets = assets; mConfiguration.setToDefaults(); mMetrics.setToDefaults(); + if (appInfo != null) { + mCompatibilityInfo = new CompatibilityInfo(appInfo); + if (DEBUG_CONFIG) { + Log.d(TAG, "compatibility for " + appInfo.packageName + " : " + mCompatibilityInfo); + } + } else { + mCompatibilityInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; + } updateConfiguration(config, metrics); assets.ensureStringBlocks(); - if (usePreloadedCache) { - mPreloadedDrawables = sPreloadedDrawables; + if (mCompatibilityInfo.isScalingRequired()) { + mPreloadedDrawables = emptySparseArray(); } else { + mPreloadedDrawables = sPreloadedDrawables; + } + } + + /** + * Creates a new resources that uses the given compatibility info. Used to create + * a context for widgets using the container's compatibility info. + * {@see ApplicationContext#createPackageCotnext}. + * @hide + */ + public Resources(AssetManager assets, DisplayMetrics metrics, + Configuration config, CompatibilityInfo info) { + mAssets = assets; + mMetrics.setToDefaults(); + mCompatibilityInfo = info; + updateConfiguration(config, metrics); + assets.ensureStringBlocks(); + if (mCompatibilityInfo.isScalingRequired()) { mPreloadedDrawables = emptySparseArray(); + } else { + mPreloadedDrawables = sPreloadedDrawables; } } @@ -1238,7 +1274,7 @@ public class Resources { return array; } - + /** * Store the newly updated configuration. */ @@ -1251,6 +1287,8 @@ public class Resources { } if (metrics != null) { mMetrics.setTo(metrics); + mMetrics.updateMetrics(mCompatibilityInfo, + mConfiguration.orientation, mConfiguration.screenLayout); } mMetrics.scaledDensity = mMetrics.density * mConfiguration.fontScale; @@ -1282,7 +1320,7 @@ public class Resources { mConfiguration.touchscreen, (int)(mMetrics.density*160), mConfiguration.keyboard, keyboardHidden, mConfiguration.navigation, width, height, - sSdkVersion); + mConfiguration.screenLayout, sSdkVersion); int N = mDrawableCache.size(); if (DEBUG_CONFIG) { Log.d(TAG, "Cleaning up drawables config changes: 0x" @@ -1297,14 +1335,14 @@ public class Resources { configChanges, cs.getChangingConfigurations())) { if (DEBUG_CONFIG) { Log.d(TAG, "FLUSHING #0x" - + Integer.toHexString(mDrawableCache.keyAt(i)) + + Long.toHexString(mDrawableCache.keyAt(i)) + " / " + cs + " with changes: 0x" + Integer.toHexString(cs.getChangingConfigurations())); } mDrawableCache.setValueAt(i, null); } else if (DEBUG_CONFIG) { Log.d(TAG, "(Keeping #0x" - + Integer.toHexString(mDrawableCache.keyAt(i)) + + Long.toHexString(mDrawableCache.keyAt(i)) + " / " + cs + " with changes: 0x" + Integer.toHexString(cs.getChangingConfigurations()) + ")"); @@ -1356,6 +1394,17 @@ public class Resources { public Configuration getConfiguration() { return mConfiguration; } + + /** + * Return the compatibility mode information for the application. + * The returned object should be treated as read-only. + * + * @return compatibility info. null if the app does not require compatibility mode. + * @hide + */ + public CompatibilityInfo getCompatibilityInfo() { + return mCompatibilityInfo; + } /** * Return a resource identifier for the given resource name. A fully @@ -1624,7 +1673,7 @@ public class Resources { } } - final int key = (value.assetCookie << 24) | value.data; + final long key = (((long) value.assetCookie) << 32) | value.data; Drawable dr = getCachedDrawable(key); if (dr != null) { @@ -1704,7 +1753,7 @@ public class Resources { return dr; } - private Drawable getCachedDrawable(int key) { + private Drawable getCachedDrawable(long key) { synchronized (mTmpValue) { WeakReference<Drawable.ConstantState> wr = mDrawableCache.get(key); if (wr != null) { // we have the key @@ -1920,5 +1969,6 @@ public class Resources { updateConfiguration(null, null); mAssets.ensureStringBlocks(); mPreloadedDrawables = sPreloadedDrawables; + mCompatibilityInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; } } diff --git a/core/java/android/database/BulkCursorToCursorAdaptor.java b/core/java/android/database/BulkCursorToCursorAdaptor.java index c26810a..cf30dd9 100644 --- a/core/java/android/database/BulkCursorToCursorAdaptor.java +++ b/core/java/android/database/BulkCursorToCursorAdaptor.java @@ -247,9 +247,11 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor { try { return mBulkCursor.respond(extras); } catch (RemoteException e) { - // This should never happen because the system kills processes that are using remote - // cursors when the provider process is killed. - throw new RuntimeException(e); + // the system kills processes that are using remote cursors when the provider process + // is killed, but this can still happen if this is being called from the system process, + // so, better to log and return an empty bundle. + Log.w(TAG, "respond() threw RemoteException, returning an empty bundle.", e); + return Bundle.EMPTY; } } } diff --git a/core/java/android/database/sqlite/SQLiteContentHelper.java b/core/java/android/database/sqlite/SQLiteContentHelper.java new file mode 100644 index 0000000..2800d86 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteContentHelper.java @@ -0,0 +1,92 @@ +/* + * 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.database.sqlite; + +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.os.MemoryFile; + +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * Some helper functions for using SQLite database to implement content providers. + * + * @hide + */ +public class SQLiteContentHelper { + + /** + * Runs an SQLite query and returns an AssetFileDescriptor for the + * blob in column 0 of the first row. If the first column does + * not contain a blob, an unspecified exception is thrown. + * + * @param db Handle to a readable database. + * @param sql SQL query, possibly with query arguments. + * @param selectionArgs Query argument values, or {@code null} for no argument. + * @return If no exception is thrown, a non-null AssetFileDescriptor is returned. + * @throws FileNotFoundException If the query returns no results or the + * value of column 0 is NULL, or if there is an error creating the + * asset file descriptor. + */ + public static AssetFileDescriptor getBlobColumnAsAssetFile(SQLiteDatabase db, String sql, + String[] selectionArgs) throws FileNotFoundException { + try { + MemoryFile file = simpleQueryForBlobMemoryFile(db, sql, selectionArgs); + if (file == null) { + throw new FileNotFoundException("No results."); + } + return AssetFileDescriptor.fromMemoryFile(file); + } catch (IOException ex) { + throw new FileNotFoundException(ex.toString()); + } + } + + /** + * Runs an SQLite query and returns a MemoryFile for the + * blob in column 0 of the first row. If the first column does + * not contain a blob, an unspecified exception is thrown. + * + * @return A memory file, or {@code null} if the query returns no results + * or the value column 0 is NULL. + * @throws IOException If there is an error creating the memory file. + */ + // TODO: make this native and use the SQLite blob API to reduce copying + private static MemoryFile simpleQueryForBlobMemoryFile(SQLiteDatabase db, String sql, + String[] selectionArgs) throws IOException { + Cursor cursor = db.rawQuery(sql, selectionArgs); + if (cursor == null) { + return null; + } + try { + if (!cursor.moveToFirst()) { + return null; + } + byte[] bytes = cursor.getBlob(0); + if (bytes == null) { + return null; + } + MemoryFile file = new MemoryFile(null, bytes.length); + file.writeBytes(bytes, 0, 0, bytes.length); + file.deactivate(); + return file; + } finally { + cursor.close(); + } + } + +} diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index ab7c827..8a63919 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -18,16 +18,15 @@ package android.database.sqlite; import android.database.Cursor; import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; import android.provider.BaseColumns; import android.text.TextUtils; -import android.util.Config; import android.util.Log; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.Map.Entry; +import java.util.regex.Pattern; /** * This is a convience class that helps build SQL queries to be sent to @@ -36,10 +35,12 @@ import java.util.Map.Entry; public class SQLiteQueryBuilder { private static final String TAG = "SQLiteQueryBuilder"; + private static final Pattern sLimitPattern = + Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?"); private Map<String, String> mProjectionMap = null; private String mTables = ""; - private StringBuilder mWhereClause = new StringBuilder(64); + private final StringBuilder mWhereClause = new StringBuilder(64); private boolean mDistinct; private SQLiteDatabase.CursorFactory mFactory; @@ -169,6 +170,9 @@ public class SQLiteQueryBuilder throw new IllegalArgumentException( "HAVING clauses are only permitted when using a groupBy clause"); } + if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) { + throw new IllegalArgumentException("invalid LIMIT clauses:" + limit); + } StringBuilder query = new StringBuilder(120); @@ -187,7 +191,7 @@ public class SQLiteQueryBuilder appendClause(query, " GROUP BY ", groupBy); appendClause(query, " HAVING ", having); appendClause(query, " ORDER BY ", orderBy); - appendClauseEscapeClause(query, " LIMIT ", limit); + appendClause(query, " LIMIT ", limit); return query.toString(); } diff --git a/core/java/android/gesture/Gesture.java b/core/java/android/gesture/Gesture.java new file mode 100755 index 0000000..2262477 --- /dev/null +++ b/core/java/android/gesture/Gesture.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2008-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.gesture; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.io.IOException; +import java.io.DataOutputStream; +import java.io.DataInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; +import java.util.ArrayList; + +/** + * A gesture can have a single or multiple strokes + */ + +public class Gesture implements Parcelable { + private static final long GESTURE_ID_BASE = System.currentTimeMillis(); + + private static final int BITMAP_RENDERING_WIDTH = 2; + + private static final boolean BITMAP_RENDERING_ANTIALIAS = true; + private static final boolean BITMAP_RENDERING_DITHER = true; + + private static int sGestureCount = 0; + + private final RectF mBoundingBox = new RectF(); + + // the same as its instance ID + private long mGestureID; + + private final ArrayList<GestureStroke> mStrokes = new ArrayList<GestureStroke>(); + + public Gesture() { + mGestureID = GESTURE_ID_BASE + sGestureCount++; + } + + void recycle() { + mStrokes.clear(); + mBoundingBox.setEmpty(); + } + + /** + * @return all the strokes of the gesture + */ + public ArrayList<GestureStroke> getStrokes() { + return mStrokes; + } + + /** + * @return the number of strokes included by this gesture + */ + public int getStrokesCount() { + return mStrokes.size(); + } + + /** + * Add a stroke to the gesture + * + * @param stroke + */ + public void addStroke(GestureStroke stroke) { + mStrokes.add(stroke); + mBoundingBox.union(stroke.boundingBox); + } + + /** + * Get the total length of the gesture. When there are multiple strokes in + * the gesture, this returns the sum of the lengths of all the strokes + * + * @return the length of the gesture + */ + public float getLength() { + int len = 0; + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + for (int i = 0; i < count; i++) { + len += strokes.get(i).length; + } + + return len; + } + + /** + * @return the bounding box of the gesture + */ + public RectF getBoundingBox() { + return mBoundingBox; + } + + public Path toPath() { + return toPath(null); + } + + public Path toPath(Path path) { + if (path == null) path = new Path(); + + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + for (int i = 0; i < count; i++) { + path.addPath(strokes.get(i).getPath()); + } + + return path; + } + + public Path toPath(int width, int height, int edge, int numSample) { + return toPath(null, width, height, edge, numSample); + } + + public Path toPath(Path path, int width, int height, int edge, int numSample) { + if (path == null) path = new Path(); + + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + for (int i = 0; i < count; i++) { + path.addPath(strokes.get(i).toPath(width - 2 * edge, height - 2 * edge, numSample)); + } + + return path; + } + + /** + * Set the id of the gesture + * + * @param id + */ + void setID(long id) { + mGestureID = id; + } + + /** + * @return the id of the gesture + */ + public long getID() { + return mGestureID; + } + + /** + * draw the gesture + * + * @param canvas + */ + void draw(Canvas canvas, Paint paint) { + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + for (int i = 0; i < count; i++) { + strokes.get(i).draw(canvas, paint); + } + } + + /** + * Create a bitmap of the gesture with a transparent background + * + * @param width width of the target bitmap + * @param height height of the target bitmap + * @param edge the edge + * @param numSample + * @param color + * @return the bitmap + */ + public Bitmap toBitmap(int width, int height, int edge, int numSample, int color) { + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + + canvas.translate(edge, edge); + + final Paint paint = new Paint(); + paint.setAntiAlias(BITMAP_RENDERING_ANTIALIAS); + paint.setDither(BITMAP_RENDERING_DITHER); + paint.setColor(color); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(BITMAP_RENDERING_WIDTH); + + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + for (int i = 0; i < count; i++) { + Path path = strokes.get(i).toPath(width - 2 * edge, height - 2 * edge, numSample); + canvas.drawPath(path, paint); + } + + return bitmap; + } + + /** + * Create a bitmap of the gesture with a transparent background + * + * @param width + * @param height + * @param inset + * @param color + * @return the bitmap + */ + public Bitmap toBitmap(int width, int height, int inset, int color) { + final Bitmap bitmap = Bitmap.createBitmap(width, height, + Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + + final Paint paint = new Paint(); + paint.setAntiAlias(BITMAP_RENDERING_ANTIALIAS); + paint.setDither(BITMAP_RENDERING_DITHER); + paint.setColor(color); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeJoin(Paint.Join.ROUND); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(BITMAP_RENDERING_WIDTH); + + final Path path = toPath(); + final RectF bounds = new RectF(); + path.computeBounds(bounds, true); + + final float sx = (width - 2 * inset) / bounds.width(); + final float sy = (height - 2 * inset) / bounds.height(); + final float scale = sx > sy ? sy : sx; + paint.setStrokeWidth(2.0f / scale); + + path.offset(-bounds.left + (width - bounds.width() * scale) / 2.0f, + -bounds.top + (height - bounds.height() * scale) / 2.0f); + + canvas.translate(inset, inset); + canvas.scale(scale, scale); + + canvas.drawPath(path, paint); + + return bitmap; + } + + void serialize(DataOutputStream out) throws IOException { + final ArrayList<GestureStroke> strokes = mStrokes; + final int count = strokes.size(); + + // Write gesture ID + out.writeLong(mGestureID); + // Write number of strokes + out.writeInt(count); + + for (int i = 0; i < count; i++) { + strokes.get(i).serialize(out); + } + } + + static Gesture deserialize(DataInputStream in) throws IOException { + final Gesture gesture = new Gesture(); + + // Gesture ID + gesture.mGestureID = in.readLong(); + // Number of strokes + final int count = in.readInt(); + + for (int i = 0; i < count; i++) { + gesture.addStroke(GestureStroke.deserialize(in)); + } + + return gesture; + } + + public static final Parcelable.Creator<Gesture> CREATOR = new Parcelable.Creator<Gesture>() { + public Gesture createFromParcel(Parcel in) { + Gesture gesture = null; + final long gestureID = in.readLong(); + + final DataInputStream inStream = new DataInputStream( + new ByteArrayInputStream(in.createByteArray())); + + try { + gesture = deserialize(inStream); + } catch (IOException e) { + Log.e(GestureConstants.LOG_TAG, "Error reading Gesture from parcel:", e); + } finally { + GestureUtilities.closeStream(inStream); + } + + if (gesture != null) { + gesture.mGestureID = gestureID; + } + + return gesture; + } + + public Gesture[] newArray(int size) { + return new Gesture[size]; + } + }; + + public void writeToParcel(Parcel out, int flags) { + out.writeLong(mGestureID); + + boolean result = false; + final ByteArrayOutputStream byteStream = + new ByteArrayOutputStream(GestureConstants.IO_BUFFER_SIZE); + final DataOutputStream outStream = new DataOutputStream(byteStream); + + try { + serialize(outStream); + result = true; + } catch (IOException e) { + Log.e(GestureConstants.LOG_TAG, "Error writing Gesture to parcel:", e); + } finally { + GestureUtilities.closeStream(outStream); + GestureUtilities.closeStream(byteStream); + } + + if (result) { + out.writeByteArray(byteStream.toByteArray()); + } + } + + public int describeContents() { + return 0; + } +} + diff --git a/core/java/android/gesture/GestureConstants.java b/core/java/android/gesture/GestureConstants.java new file mode 100644 index 0000000..230db0c --- /dev/null +++ b/core/java/android/gesture/GestureConstants.java @@ -0,0 +1,26 @@ +/* + * 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.gesture; + +interface GestureConstants { + static final int STROKE_STRING_BUFFER_SIZE = 1024; + static final int STROKE_POINT_BUFFER_SIZE = 100; // number of points + + static final int IO_BUFFER_SIZE = 32 * 1024; // 32K + + static final String LOG_TAG = "Gestures"; +} diff --git a/core/java/android/gesture/GestureLibraries.java b/core/java/android/gesture/GestureLibraries.java new file mode 100644 index 0000000..6d6c156 --- /dev/null +++ b/core/java/android/gesture/GestureLibraries.java @@ -0,0 +1,143 @@ +/* + * 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.gesture; + +import android.util.Log; +import static android.gesture.GestureConstants.*; +import android.content.Context; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.FileInputStream; +import java.io.InputStream; +import java.lang.ref.WeakReference; + +public final class GestureLibraries { + private GestureLibraries() { + } + + public static GestureLibrary fromFile(String path) { + return fromFile(new File(path)); + } + + public static GestureLibrary fromFile(File path) { + return new FileGestureLibrary(path); + } + + public static GestureLibrary fromPrivateFile(Context context, String name) { + return fromFile(context.getFileStreamPath(name)); + } + + public static GestureLibrary fromRawResource(Context context, int resourceId) { + return new ResourceGestureLibrary(context, resourceId); + } + + private static class FileGestureLibrary extends GestureLibrary { + private final File mPath; + + public FileGestureLibrary(File path) { + mPath = path; + } + + @Override + public boolean isReadOnly() { + return !mPath.canWrite(); + } + + public boolean save() { + if (!mStore.hasChanged()) return true; + + final File file = mPath; + + final File parentFile = file.getParentFile(); + if (!parentFile.exists()) { + if (!parentFile.mkdirs()) { + return false; + } + } + + boolean result = false; + try { + //noinspection ResultOfMethodCallIgnored + file.createNewFile(); + mStore.save(new FileOutputStream(file), true); + result = true; + } catch (FileNotFoundException e) { + Log.d(LOG_TAG, "Could not save the gesture library in " + mPath, e); + } catch (IOException e) { + Log.d(LOG_TAG, "Could not save the gesture library in " + mPath, e); + } + + return result; + } + + public boolean load() { + boolean result = false; + final File file = mPath; + if (file.exists() && file.canRead()) { + try { + mStore.load(new FileInputStream(file), true); + result = true; + } catch (FileNotFoundException e) { + Log.d(LOG_TAG, "Could not load the gesture library from " + mPath, e); + } catch (IOException e) { + Log.d(LOG_TAG, "Could not load the gesture library from " + mPath, e); + } + } + + return result; + } + } + + private static class ResourceGestureLibrary extends GestureLibrary { + private final WeakReference<Context> mContext; + private final int mResourceId; + + public ResourceGestureLibrary(Context context, int resourceId) { + mContext = new WeakReference<Context>(context); + mResourceId = resourceId; + } + + @Override + public boolean isReadOnly() { + return true; + } + + public boolean save() { + return false; + } + + public boolean load() { + boolean result = false; + final Context context = mContext.get(); + if (context != null) { + final InputStream in = context.getResources().openRawResource(mResourceId); + try { + mStore.load(in, true); + result = true; + } catch (IOException e) { + Log.d(LOG_TAG, "Could not load the gesture library from raw resource " + + context.getResources().getResourceName(mResourceId), e); + } + } + + return result; + } + } +} diff --git a/core/java/android/gesture/GestureLibrary.java b/core/java/android/gesture/GestureLibrary.java new file mode 100644 index 0000000..a29c2c8 --- /dev/null +++ b/core/java/android/gesture/GestureLibrary.java @@ -0,0 +1,81 @@ +/* + * 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.gesture; + +import java.util.Set; +import java.util.ArrayList; + +public abstract class GestureLibrary { + protected final GestureStore mStore; + + protected GestureLibrary() { + mStore = new GestureStore(); + } + + public abstract boolean save(); + + public abstract boolean load(); + + public boolean isReadOnly() { + return false; + } + + public Learner getLearner() { + return mStore.getLearner(); + } + + public void setOrientationStyle(int style) { + mStore.setOrientationStyle(style); + } + + public int getOrientationStyle() { + return mStore.getOrientationStyle(); + } + + public void setSequenceType(int type) { + mStore.setSequenceType(type); + } + + public int getSequenceType() { + return mStore.getSequenceType(); + } + + public Set<String> getGestureEntries() { + return mStore.getGestureEntries(); + } + + public ArrayList<Prediction> recognize(Gesture gesture) { + return mStore.recognize(gesture); + } + + public void addGesture(String entryName, Gesture gesture) { + mStore.addGesture(entryName, gesture); + } + + public void removeGesture(String entryName, Gesture gesture) { + mStore.removeGesture(entryName, gesture); + } + + public void removeEntry(String entryName) { + mStore.removeEntry(entryName); + } + + public ArrayList<Gesture> getGestures(String entryName) { + return mStore.getGestures(entryName); + } +} diff --git a/core/java/android/gesture/GestureOverlayView.java b/core/java/android/gesture/GestureOverlayView.java new file mode 100755 index 0000000..5bfdcc4 --- /dev/null +++ b/core/java/android/gesture/GestureOverlayView.java @@ -0,0 +1,793 @@ +/* + * 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.gesture; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.animation.AnimationUtils; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.FrameLayout; +import android.os.SystemClock; +import com.android.internal.R; + +import java.util.ArrayList; + +/** + * A transparent overlay for gesture input that can be placed on top of other + * widgets or contain other widgets. + * + * @attr ref android.R.styleable#GestureOverlayView_eventsInterceptionEnabled + * @attr ref android.R.styleable#GestureOverlayView_fadeDuration + * @attr ref android.R.styleable#GestureOverlayView_fadeOffset + * @attr ref android.R.styleable#GestureOverlayView_fadeEnabled + * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeWidth + * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeAngleThreshold + * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeLengthThreshold + * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeSquarenessThreshold + * @attr ref android.R.styleable#GestureOverlayView_gestureStrokeType + * @attr ref android.R.styleable#GestureOverlayView_gestureColor + * @attr ref android.R.styleable#GestureOverlayView_orientation + * @attr ref android.R.styleable#GestureOverlayView_uncertainGestureColor + */ +public class GestureOverlayView extends FrameLayout { + public static final int GESTURE_STROKE_TYPE_SINGLE = 0; + public static final int GESTURE_STROKE_TYPE_MULTIPLE = 1; + + public static final int ORIENTATION_HORIZONTAL = 0; + public static final int ORIENTATION_VERTICAL = 1; + + private static final int FADE_ANIMATION_RATE = 16; + private static final boolean GESTURE_RENDERING_ANTIALIAS = true; + private static final boolean DITHER_FLAG = true; + + private final Paint mGesturePaint = new Paint(); + + private long mFadeDuration = 150; + private long mFadeOffset = 420; + private long mFadingStart; + private boolean mFadingHasStarted; + private boolean mFadeEnabled = true; + + private int mCurrentColor; + private int mCertainGestureColor = 0xFFFFFF00; + private int mUncertainGestureColor = 0x48FFFF00; + private float mGestureStrokeWidth = 12.0f; + private int mInvalidateExtraBorder = 10; + + private int mGestureStrokeType = GESTURE_STROKE_TYPE_SINGLE; + private float mGestureStrokeLengthThreshold = 50.0f; + private float mGestureStrokeSquarenessTreshold = 0.275f; + private float mGestureStrokeAngleThreshold = 40.0f; + + private int mOrientation = ORIENTATION_VERTICAL; + + private final Rect mInvalidRect = new Rect(); + private final Path mPath = new Path(); + private boolean mGestureVisible = true; + + private float mX; + private float mY; + + private float mCurveEndX; + private float mCurveEndY; + + private float mTotalLength; + private boolean mIsGesturing = false; + private boolean mPreviousWasGesturing = false; + private boolean mInterceptEvents = true; + private boolean mIsListeningForGestures; + private boolean mResetGesture; + + // current gesture + private Gesture mCurrentGesture; + private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<GesturePoint>(100); + + // TODO: Make this a list of WeakReferences + private final ArrayList<OnGestureListener> mOnGestureListeners = + new ArrayList<OnGestureListener>(); + // TODO: Make this a list of WeakReferences + private final ArrayList<OnGesturePerformedListener> mOnGesturePerformedListeners = + new ArrayList<OnGesturePerformedListener>(); + // TODO: Make this a list of WeakReferences + private final ArrayList<OnGesturingListener> mOnGesturingListeners = + new ArrayList<OnGesturingListener>(); + + private boolean mHandleGestureActions; + + // fading out effect + private boolean mIsFadingOut = false; + private float mFadingAlpha = 1.0f; + private final AccelerateDecelerateInterpolator mInterpolator = + new AccelerateDecelerateInterpolator(); + + private final FadeOutRunnable mFadingOut = new FadeOutRunnable(); + + public GestureOverlayView(Context context) { + super(context); + init(); + } + + public GestureOverlayView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.gestureOverlayViewStyle); + } + + public GestureOverlayView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.GestureOverlayView, defStyle, 0); + + mGestureStrokeWidth = a.getFloat(R.styleable.GestureOverlayView_gestureStrokeWidth, + mGestureStrokeWidth); + mInvalidateExtraBorder = Math.max(1, ((int) mGestureStrokeWidth) - 1); + mCertainGestureColor = a.getColor(R.styleable.GestureOverlayView_gestureColor, + mCertainGestureColor); + mUncertainGestureColor = a.getColor(R.styleable.GestureOverlayView_uncertainGestureColor, + mUncertainGestureColor); + mFadeDuration = a.getInt(R.styleable.GestureOverlayView_fadeDuration, (int) mFadeDuration); + mFadeOffset = a.getInt(R.styleable.GestureOverlayView_fadeOffset, (int) mFadeOffset); + mGestureStrokeType = a.getInt(R.styleable.GestureOverlayView_gestureStrokeType, + mGestureStrokeType); + mGestureStrokeLengthThreshold = a.getFloat( + R.styleable.GestureOverlayView_gestureStrokeLengthThreshold, + mGestureStrokeLengthThreshold); + mGestureStrokeAngleThreshold = a.getFloat( + R.styleable.GestureOverlayView_gestureStrokeAngleThreshold, + mGestureStrokeAngleThreshold); + mGestureStrokeSquarenessTreshold = a.getFloat( + R.styleable.GestureOverlayView_gestureStrokeSquarenessThreshold, + mGestureStrokeSquarenessTreshold); + mInterceptEvents = a.getBoolean(R.styleable.GestureOverlayView_eventsInterceptionEnabled, + mInterceptEvents); + mFadeEnabled = a.getBoolean(R.styleable.GestureOverlayView_fadeEnabled, + mFadeEnabled); + mOrientation = a.getInt(R.styleable.GestureOverlayView_orientation, mOrientation); + + a.recycle(); + + init(); + } + + private void init() { + setWillNotDraw(false); + + final Paint gesturePaint = mGesturePaint; + gesturePaint.setAntiAlias(GESTURE_RENDERING_ANTIALIAS); + gesturePaint.setColor(mCertainGestureColor); + gesturePaint.setStyle(Paint.Style.STROKE); + gesturePaint.setStrokeJoin(Paint.Join.ROUND); + gesturePaint.setStrokeCap(Paint.Cap.ROUND); + gesturePaint.setStrokeWidth(mGestureStrokeWidth); + gesturePaint.setDither(DITHER_FLAG); + + mCurrentColor = mCertainGestureColor; + setPaintAlpha(255); + } + + public ArrayList<GesturePoint> getCurrentStroke() { + return mStrokeBuffer; + } + + public int getOrientation() { + return mOrientation; + } + + public void setOrientation(int orientation) { + mOrientation = orientation; + } + + public void setGestureColor(int color) { + mCertainGestureColor = color; + } + + public void setUncertainGestureColor(int color) { + mUncertainGestureColor = color; + } + + public int getUncertainGestureColor() { + return mUncertainGestureColor; + } + + public int getGestureColor() { + return mCertainGestureColor; + } + + public float getGestureStrokeWidth() { + return mGestureStrokeWidth; + } + + public void setGestureStrokeWidth(float gestureStrokeWidth) { + mGestureStrokeWidth = gestureStrokeWidth; + mInvalidateExtraBorder = Math.max(1, ((int) gestureStrokeWidth) - 1); + mGesturePaint.setStrokeWidth(gestureStrokeWidth); + } + + public int getGestureStrokeType() { + return mGestureStrokeType; + } + + public void setGestureStrokeType(int gestureStrokeType) { + mGestureStrokeType = gestureStrokeType; + } + + public float getGestureStrokeLengthThreshold() { + return mGestureStrokeLengthThreshold; + } + + public void setGestureStrokeLengthThreshold(float gestureStrokeLengthThreshold) { + mGestureStrokeLengthThreshold = gestureStrokeLengthThreshold; + } + + public float getGestureStrokeSquarenessTreshold() { + return mGestureStrokeSquarenessTreshold; + } + + public void setGestureStrokeSquarenessTreshold(float gestureStrokeSquarenessTreshold) { + mGestureStrokeSquarenessTreshold = gestureStrokeSquarenessTreshold; + } + + public float getGestureStrokeAngleThreshold() { + return mGestureStrokeAngleThreshold; + } + + public void setGestureStrokeAngleThreshold(float gestureStrokeAngleThreshold) { + mGestureStrokeAngleThreshold = gestureStrokeAngleThreshold; + } + + public boolean isEventsInterceptionEnabled() { + return mInterceptEvents; + } + + public void setEventsInterceptionEnabled(boolean enabled) { + mInterceptEvents = enabled; + } + + public boolean isFadeEnabled() { + return mFadeEnabled; + } + + public void setFadeEnabled(boolean fadeEnabled) { + mFadeEnabled = fadeEnabled; + } + + public Gesture getGesture() { + return mCurrentGesture; + } + + public void setGesture(Gesture gesture) { + if (mCurrentGesture != null) { + clear(false); + } + + setCurrentColor(mCertainGestureColor); + mCurrentGesture = gesture; + + final Path path = mCurrentGesture.toPath(); + final RectF bounds = new RectF(); + path.computeBounds(bounds, true); + + // TODO: The path should also be scaled to fit inside this view + mPath.rewind(); + mPath.addPath(path, -bounds.left + (getWidth() - bounds.width()) / 2.0f, + -bounds.top + (getHeight() - bounds.height()) / 2.0f); + + mResetGesture = true; + + invalidate(); + } + + public Path getGesturePath() { + return mPath; + } + + public Path getGesturePath(Path path) { + path.set(mPath); + return path; + } + + public boolean isGestureVisible() { + return mGestureVisible; + } + + public void setGestureVisible(boolean visible) { + mGestureVisible = visible; + } + + public long getFadeOffset() { + return mFadeOffset; + } + + public void setFadeOffset(long fadeOffset) { + mFadeOffset = fadeOffset; + } + + public void addOnGestureListener(OnGestureListener listener) { + mOnGestureListeners.add(listener); + } + + public void removeOnGestureListener(OnGestureListener listener) { + mOnGestureListeners.remove(listener); + } + + public void removeAllOnGestureListeners() { + mOnGestureListeners.clear(); + } + + public void addOnGesturePerformedListener(OnGesturePerformedListener listener) { + mOnGesturePerformedListeners.add(listener); + if (mOnGesturePerformedListeners.size() > 0) { + mHandleGestureActions = true; + } + } + + public void removeOnGesturePerformedListener(OnGesturePerformedListener listener) { + mOnGesturePerformedListeners.remove(listener); + if (mOnGesturePerformedListeners.size() <= 0) { + mHandleGestureActions = false; + } + } + + public void removeAllOnGesturePerformedListeners() { + mOnGesturePerformedListeners.clear(); + mHandleGestureActions = false; + } + + public void addOnGesturingListener(OnGesturingListener listener) { + mOnGesturingListeners.add(listener); + } + + public void removeOnGesturingListener(OnGesturingListener listener) { + mOnGesturingListeners.remove(listener); + } + + public void removeAllOnGesturingListeners() { + mOnGesturingListeners.clear(); + } + + public boolean isGesturing() { + return mIsGesturing; + } + + private void setCurrentColor(int color) { + mCurrentColor = color; + if (mFadingHasStarted) { + setPaintAlpha((int) (255 * mFadingAlpha)); + } else { + setPaintAlpha(255); + } + invalidate(); + } + + /** + * @hide + */ + public Paint getGesturePaint() { + return mGesturePaint; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + if (mCurrentGesture != null && mGestureVisible) { + canvas.drawPath(mPath, mGesturePaint); + } + } + + private void setPaintAlpha(int alpha) { + alpha += alpha >> 7; + final int baseAlpha = mCurrentColor >>> 24; + final int useAlpha = baseAlpha * alpha >> 8; + mGesturePaint.setColor((mCurrentColor << 8 >>> 8) | (useAlpha << 24)); + } + + public void clear(boolean animated) { + clear(animated, false, true); + } + + private void clear(boolean animated, boolean fireActionPerformed, boolean immediate) { + setPaintAlpha(255); + removeCallbacks(mFadingOut); + mResetGesture = false; + mFadingOut.fireActionPerformed = fireActionPerformed; + mFadingOut.resetMultipleStrokes = false; + + if (animated && mCurrentGesture != null) { + mFadingAlpha = 1.0f; + mIsFadingOut = true; + mFadingHasStarted = false; + mFadingStart = AnimationUtils.currentAnimationTimeMillis() + mFadeOffset; + + postDelayed(mFadingOut, mFadeOffset); + } else { + mFadingAlpha = 1.0f; + mIsFadingOut = false; + mFadingHasStarted = false; + + if (immediate) { + mCurrentGesture = null; + mPath.rewind(); + invalidate(); + } else if (fireActionPerformed) { + postDelayed(mFadingOut, mFadeOffset); + } else if (mGestureStrokeType == GESTURE_STROKE_TYPE_MULTIPLE) { + mFadingOut.resetMultipleStrokes = true; + postDelayed(mFadingOut, mFadeOffset); + } else { + mCurrentGesture = null; + mPath.rewind(); + invalidate(); + } + } + } + + public void cancelClearAnimation() { + setPaintAlpha(255); + mIsFadingOut = false; + mFadingHasStarted = false; + removeCallbacks(mFadingOut); + mPath.rewind(); + mCurrentGesture = null; + } + + public void cancelGesture() { + mIsListeningForGestures = false; + + // add the stroke to the current gesture + mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer)); + + // pass the event to handlers + final long now = SystemClock.uptimeMillis(); + final MotionEvent event = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGestureCancelled(this, event); + } + + event.recycle(); + + clear(false); + mIsGesturing = false; + mPreviousWasGesturing = false; + mStrokeBuffer.clear(); + + final ArrayList<OnGesturingListener> otherListeners = mOnGesturingListeners; + count = otherListeners.size(); + for (int i = 0; i < count; i++) { + otherListeners.get(i).onGesturingEnded(this); + } + } + + @Override + protected void onDetachedFromWindow() { + cancelClearAnimation(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (isEnabled()) { + final boolean cancelDispatch = (mIsGesturing || (mCurrentGesture != null && + mCurrentGesture.getStrokesCount() > 0 && mPreviousWasGesturing)) && + mInterceptEvents; + + processEvent(event); + + if (cancelDispatch) { + event.setAction(MotionEvent.ACTION_CANCEL); + } + + super.dispatchTouchEvent(event); + + return true; + } + + return super.dispatchTouchEvent(event); + } + + private boolean processEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + touchDown(event); + invalidate(); + return true; + case MotionEvent.ACTION_MOVE: + if (mIsListeningForGestures) { + Rect rect = touchMove(event); + if (rect != null) { + invalidate(rect); + } + return true; + } + break; + case MotionEvent.ACTION_UP: + if (mIsListeningForGestures) { + touchUp(event, false); + invalidate(); + return true; + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsListeningForGestures) { + touchUp(event, true); + invalidate(); + return true; + } + } + + return false; + } + + private void touchDown(MotionEvent event) { + mIsListeningForGestures = true; + + float x = event.getX(); + float y = event.getY(); + + mX = x; + mY = y; + + mTotalLength = 0; + mIsGesturing = false; + + if (mGestureStrokeType == GESTURE_STROKE_TYPE_SINGLE || mResetGesture) { + if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor); + mResetGesture = false; + mCurrentGesture = null; + mPath.rewind(); + } else if (mCurrentGesture == null || mCurrentGesture.getStrokesCount() == 0) { + if (mHandleGestureActions) setCurrentColor(mUncertainGestureColor); + } + + // if there is fading out going on, stop it. + if (mFadingHasStarted) { + cancelClearAnimation(); + } else if (mIsFadingOut) { + setPaintAlpha(255); + mIsFadingOut = false; + mFadingHasStarted = false; + removeCallbacks(mFadingOut); + } + + if (mCurrentGesture == null) { + mCurrentGesture = new Gesture(); + } + + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + mPath.moveTo(x, y); + + final int border = mInvalidateExtraBorder; + mInvalidRect.set((int) x - border, (int) y - border, (int) x + border, (int) y + border); + + mCurveEndX = x; + mCurveEndY = y; + + // pass the event to handlers + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + final int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGestureStarted(this, event); + } + } + + private Rect touchMove(MotionEvent event) { + Rect areaToRefresh = null; + + final float x = event.getX(); + final float y = event.getY(); + + final float previousX = mX; + final float previousY = mY; + + final float dx = Math.abs(x - previousX); + final float dy = Math.abs(y - previousY); + + if (dx >= GestureStroke.TOUCH_TOLERANCE || dy >= GestureStroke.TOUCH_TOLERANCE) { + areaToRefresh = mInvalidRect; + + // start with the curve end + final int border = mInvalidateExtraBorder; + areaToRefresh.set((int) mCurveEndX - border, (int) mCurveEndY - border, + (int) mCurveEndX + border, (int) mCurveEndY + border); + + float cX = mCurveEndX = (x + previousX) / 2; + float cY = mCurveEndY = (y + previousY) / 2; + + mPath.quadTo(previousX, previousY, cX, cY); + + // union with the control point of the new curve + areaToRefresh.union((int) previousX - border, (int) previousY - border, + (int) previousX + border, (int) previousY + border); + + // union with the end point of the new curve + areaToRefresh.union((int) cX - border, (int) cY - border, + (int) cX + border, (int) cY + border); + + mX = x; + mY = y; + + mStrokeBuffer.add(new GesturePoint(x, y, event.getEventTime())); + + if (mHandleGestureActions && !mIsGesturing) { + mTotalLength += (float) Math.sqrt(dx * dx + dy * dy); + + if (mTotalLength > mGestureStrokeLengthThreshold) { + final OrientedBoundingBox box = + GestureUtilities.computeOrientedBoundingBox(mStrokeBuffer); + + float angle = Math.abs(box.orientation); + if (angle > 90) { + angle = 180 - angle; + } + + if (box.squareness > mGestureStrokeSquarenessTreshold || + (mOrientation == ORIENTATION_VERTICAL ? + angle < mGestureStrokeAngleThreshold : + angle > mGestureStrokeAngleThreshold)) { + + mIsGesturing = true; + setCurrentColor(mCertainGestureColor); + + final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners; + int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGesturingStarted(this); + } + } + } + } + + // pass the event to handlers + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + final int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGesture(this, event); + } + } + + return areaToRefresh; + } + + private void touchUp(MotionEvent event, boolean cancel) { + mIsListeningForGestures = false; + + // A gesture wasn't started or was cancelled + if (mCurrentGesture != null) { + // add the stroke to the current gesture + mCurrentGesture.addStroke(new GestureStroke(mStrokeBuffer)); + + if (!cancel) { + // pass the event to handlers + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGestureEnded(this, event); + } + + clear(mHandleGestureActions && mFadeEnabled, mHandleGestureActions && mIsGesturing, + false); + } else { + cancelGesture(event); + + } + } else { + cancelGesture(event); + } + + mStrokeBuffer.clear(); + mPreviousWasGesturing = mIsGesturing; + mIsGesturing = false; + + final ArrayList<OnGesturingListener> listeners = mOnGesturingListeners; + int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGesturingEnded(this); + } + } + + private void cancelGesture(MotionEvent event) { + // pass the event to handlers + final ArrayList<OnGestureListener> listeners = mOnGestureListeners; + final int count = listeners.size(); + for (int i = 0; i < count; i++) { + listeners.get(i).onGestureCancelled(this, event); + } + + clear(false); + } + + private void fireOnGesturePerformed() { + final ArrayList<OnGesturePerformedListener> actionListeners = mOnGesturePerformedListeners; + final int count = actionListeners.size(); + for (int i = 0; i < count; i++) { + actionListeners.get(i).onGesturePerformed(GestureOverlayView.this, mCurrentGesture); + } + } + + private class FadeOutRunnable implements Runnable { + boolean fireActionPerformed; + boolean resetMultipleStrokes; + + public void run() { + if (mIsFadingOut) { + final long now = AnimationUtils.currentAnimationTimeMillis(); + final long duration = now - mFadingStart; + + if (duration > mFadeDuration) { + if (fireActionPerformed) { + fireOnGesturePerformed(); + } + + mPreviousWasGesturing = false; + mIsFadingOut = false; + mFadingHasStarted = false; + mPath.rewind(); + mCurrentGesture = null; + setPaintAlpha(255); + } else { + mFadingHasStarted = true; + float interpolatedTime = Math.max(0.0f, + Math.min(1.0f, duration / (float) mFadeDuration)); + mFadingAlpha = 1.0f - mInterpolator.getInterpolation(interpolatedTime); + setPaintAlpha((int) (255 * mFadingAlpha)); + postDelayed(this, FADE_ANIMATION_RATE); + } + } else if (resetMultipleStrokes) { + mResetGesture = true; + } else { + fireOnGesturePerformed(); + + mFadingHasStarted = false; + mPath.rewind(); + mCurrentGesture = null; + mPreviousWasGesturing = false; + setPaintAlpha(255); + } + + invalidate(); + } + } + + public static interface OnGesturingListener { + void onGesturingStarted(GestureOverlayView overlay); + + void onGesturingEnded(GestureOverlayView overlay); + } + + public static interface OnGestureListener { + void onGestureStarted(GestureOverlayView overlay, MotionEvent event); + + void onGesture(GestureOverlayView overlay, MotionEvent event); + + void onGestureEnded(GestureOverlayView overlay, MotionEvent event); + + void onGestureCancelled(GestureOverlayView overlay, MotionEvent event); + } + + public static interface OnGesturePerformedListener { + void onGesturePerformed(GestureOverlayView overlay, Gesture gesture); + } +} diff --git a/core/java/android/gesture/GesturePoint.java b/core/java/android/gesture/GesturePoint.java new file mode 100644 index 0000000..3698011 --- /dev/null +++ b/core/java/android/gesture/GesturePoint.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2008-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.gesture; + +import java.io.DataInputStream; +import java.io.IOException; + +/** + * A timed point of a gesture stroke + */ + +public class GesturePoint { + public final float x; + public final float y; + + public final long timestamp; + + public GesturePoint(float x, float y, long t) { + this.x = x; + this.y = y; + timestamp = t; + } + + static GesturePoint deserialize(DataInputStream in) throws IOException { + // Read X and Y + final float x = in.readFloat(); + final float y = in.readFloat(); + // Read timestamp + final long timeStamp = in.readLong(); + return new GesturePoint(x, y, timeStamp); + } +} diff --git a/core/java/android/gesture/GestureStore.java b/core/java/android/gesture/GestureStore.java new file mode 100644 index 0000000..5f1a445 --- /dev/null +++ b/core/java/android/gesture/GestureStore.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2008-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.gesture; + +import android.util.Log; +import android.os.SystemClock; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.DataOutputStream; +import java.io.DataInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Set; +import java.util.Map; + +import static android.gesture.GestureConstants.LOG_TAG; + +/** + * GestureLibrary maintains gesture examples and makes predictions on a new + * gesture + */ +// +// File format for GestureStore: +// +// Nb. bytes Java type Description +// ----------------------------------- +// Header +// 2 bytes short File format version number +// 4 bytes int Number of entries +// Entry +// X bytes UTF String Entry name +// 4 bytes int Number of gestures +// Gesture +// 8 bytes long Gesture ID +// 4 bytes int Number of strokes +// Stroke +// 4 bytes int Number of points +// Point +// 4 bytes float X coordinate of the point +// 4 bytes float Y coordinate of the point +// 8 bytes long Time stamp +// +public class GestureStore { + public static final int SEQUENCE_INVARIANT = 1; + // when SEQUENCE_SENSITIVE is used, only single stroke gestures are currently allowed + public static final int SEQUENCE_SENSITIVE = 2; + + // ORIENTATION_SENSITIVE and ORIENTATION_INVARIANT are only for SEQUENCE_SENSITIVE gestures + public static final int ORIENTATION_INVARIANT = 1; + public static final int ORIENTATION_SENSITIVE = 2; + + private static final short FILE_FORMAT_VERSION = 1; + + private static final boolean PROFILE_LOADING_SAVING = false; + + private int mSequenceType = SEQUENCE_SENSITIVE; + private int mOrientationStyle = ORIENTATION_SENSITIVE; + + private final HashMap<String, ArrayList<Gesture>> mNamedGestures = + new HashMap<String, ArrayList<Gesture>>(); + + private Learner mClassifier; + + private boolean mChanged = false; + + public GestureStore() { + mClassifier = new InstanceLearner(); + } + + /** + * Specify how the gesture library will handle orientation. + * Use ORIENTATION_INVARIANT or ORIENTATION_SENSITIVE + * + * @param style + */ + public void setOrientationStyle(int style) { + mOrientationStyle = style; + } + + public int getOrientationStyle() { + return mOrientationStyle; + } + + /** + * @param type SEQUENCE_INVARIANT or SEQUENCE_SENSITIVE + */ + public void setSequenceType(int type) { + mSequenceType = type; + } + + /** + * @return SEQUENCE_INVARIANT or SEQUENCE_SENSITIVE + */ + public int getSequenceType() { + return mSequenceType; + } + + /** + * Get all the gesture entry names in the library + * + * @return a set of strings + */ + public Set<String> getGestureEntries() { + return mNamedGestures.keySet(); + } + + /** + * Recognize a gesture + * + * @param gesture the query + * @return a list of predictions of possible entries for a given gesture + */ + public ArrayList<Prediction> recognize(Gesture gesture) { + Instance instance = Instance.createInstance(mSequenceType, + mOrientationStyle, gesture, null); + return mClassifier.classify(mSequenceType, instance.vector); + } + + /** + * Add a gesture for the entry + * + * @param entryName entry name + * @param gesture + */ + public void addGesture(String entryName, Gesture gesture) { + if (entryName == null || entryName.length() == 0) { + return; + } + ArrayList<Gesture> gestures = mNamedGestures.get(entryName); + if (gestures == null) { + gestures = new ArrayList<Gesture>(); + mNamedGestures.put(entryName, gestures); + } + gestures.add(gesture); + mClassifier.addInstance( + Instance.createInstance(mSequenceType, mOrientationStyle, gesture, entryName)); + mChanged = true; + } + + /** + * Remove a gesture from the library. If there are no more gestures for the + * given entry, the gesture entry will be removed. + * + * @param entryName entry name + * @param gesture + */ + public void removeGesture(String entryName, Gesture gesture) { + ArrayList<Gesture> gestures = mNamedGestures.get(entryName); + if (gestures == null) { + return; + } + + gestures.remove(gesture); + + // if there are no more samples, remove the entry automatically + if (gestures.isEmpty()) { + mNamedGestures.remove(entryName); + } + + mClassifier.removeInstance(gesture.getID()); + + mChanged = true; + } + + /** + * Remove a entry of gestures + * + * @param entryName the entry name + */ + public void removeEntry(String entryName) { + mNamedGestures.remove(entryName); + mClassifier.removeInstances(entryName); + mChanged = true; + } + + /** + * Get all the gestures of an entry + * + * @param entryName + * @return the list of gestures that is under this name + */ + public ArrayList<Gesture> getGestures(String entryName) { + ArrayList<Gesture> gestures = mNamedGestures.get(entryName); + if (gestures != null) { + return new ArrayList<Gesture>(gestures); + } else { + return null; + } + } + + public boolean hasChanged() { + return mChanged; + } + + /** + * Save the gesture library + */ + public void save(OutputStream stream) throws IOException { + save(stream, false); + } + + public void save(OutputStream stream, boolean closeStream) throws IOException { + DataOutputStream out = null; + + try { + long start; + if (PROFILE_LOADING_SAVING) { + start = SystemClock.elapsedRealtime(); + } + + final HashMap<String, ArrayList<Gesture>> maps = mNamedGestures; + + out = new DataOutputStream((stream instanceof BufferedOutputStream) ? stream : + new BufferedOutputStream(stream, GestureConstants.IO_BUFFER_SIZE)); + // Write version number + out.writeShort(FILE_FORMAT_VERSION); + // Write number of entries + out.writeInt(maps.size()); + + for (Map.Entry<String, ArrayList<Gesture>> entry : maps.entrySet()) { + final String key = entry.getKey(); + final ArrayList<Gesture> examples = entry.getValue(); + final int count = examples.size(); + + // Write entry name + out.writeUTF(key); + // Write number of examples for this entry + out.writeInt(count); + + for (int i = 0; i < count; i++) { + examples.get(i).serialize(out); + } + } + + out.flush(); + + if (PROFILE_LOADING_SAVING) { + long end = SystemClock.elapsedRealtime(); + Log.d(LOG_TAG, "Saving gestures library = " + (end - start) + " ms"); + } + + mChanged = false; + } finally { + if (closeStream) GestureUtilities.closeStream(out); + } + } + + /** + * Load the gesture library + */ + public void load(InputStream stream) throws IOException { + load(stream, false); + } + + public void load(InputStream stream, boolean closeStream) throws IOException { + DataInputStream in = null; + try { + in = new DataInputStream((stream instanceof BufferedInputStream) ? stream : + new BufferedInputStream(stream, GestureConstants.IO_BUFFER_SIZE)); + + long start; + if (PROFILE_LOADING_SAVING) { + start = SystemClock.elapsedRealtime(); + } + + // Read file format version number + final short versionNumber = in.readShort(); + switch (versionNumber) { + case 1: + readFormatV1(in); + break; + } + + if (PROFILE_LOADING_SAVING) { + long end = SystemClock.elapsedRealtime(); + Log.d(LOG_TAG, "Loading gestures library = " + (end - start) + " ms"); + } + } finally { + if (closeStream) GestureUtilities.closeStream(in); + } + } + + private void readFormatV1(DataInputStream in) throws IOException { + final Learner classifier = mClassifier; + final HashMap<String, ArrayList<Gesture>> namedGestures = mNamedGestures; + namedGestures.clear(); + + // Number of entries in the library + final int entriesCount = in.readInt(); + + for (int i = 0; i < entriesCount; i++) { + // Entry name + final String name = in.readUTF(); + // Number of gestures + final int gestureCount = in.readInt(); + + final ArrayList<Gesture> gestures = new ArrayList<Gesture>(gestureCount); + for (int j = 0; j < gestureCount; j++) { + final Gesture gesture = Gesture.deserialize(in); + gestures.add(gesture); + classifier.addInstance( + Instance.createInstance(mSequenceType, mOrientationStyle, gesture, name)); + } + + namedGestures.put(name, gestures); + } + } + + Learner getLearner() { + return mClassifier; + } +} diff --git a/core/java/android/gesture/GestureStroke.java b/core/java/android/gesture/GestureStroke.java new file mode 100644 index 0000000..598eb85 --- /dev/null +++ b/core/java/android/gesture/GestureStroke.java @@ -0,0 +1,229 @@ +/* + * 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.gesture; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; + +import java.io.IOException; +import java.io.DataOutputStream; +import java.io.DataInputStream; +import java.util.ArrayList; + +/** + * A gesture stroke started on a touch down and ended on a touch up. + */ +public class GestureStroke { + static final float TOUCH_TOLERANCE = 8; + + public final RectF boundingBox; + + public final float length; + public final float[] points; + + private final long[] timestamps; + private Path mCachedPath; + + /** + * Construct a gesture stroke from a list of gesture points + * + * @param points + */ + public GestureStroke(ArrayList<GesturePoint> points) { + final int count = points.size(); + final float[] tmpPoints = new float[count * 2]; + final long[] times = new long[count]; + + RectF bx = null; + float len = 0; + int index = 0; + + for (int i = 0; i < count; i++) { + final GesturePoint p = points.get(i); + tmpPoints[i * 2] = p.x; + tmpPoints[i * 2 + 1] = p.y; + times[index] = p.timestamp; + + if (bx == null) { + bx = new RectF(); + bx.top = p.y; + bx.left = p.x; + bx.right = p.x; + bx.bottom = p.y; + len = 0; + } else { + len += Math.sqrt(Math.pow(p.x - tmpPoints[(i - 1) * 2], 2) + + Math.pow(p.y - tmpPoints[(i -1 ) * 2 + 1], 2)); + bx.union(p.x, p.y); + } + index++; + } + + timestamps = times; + this.points = tmpPoints; + boundingBox = bx; + length = len; + } + + /** + * Draw the gesture with a given canvas and paint + * + * @param canvas + */ + void draw(Canvas canvas, Paint paint) { + if (mCachedPath == null) { + makePath(); + } + + canvas.drawPath(mCachedPath, paint); + } + + public Path getPath() { + if (mCachedPath == null) { + makePath(); + } + + return mCachedPath; + } + + private void makePath() { + final float[] localPoints = points; + final int count = localPoints.length; + + Path path = null; + + float mX = 0; + float mY = 0; + + for (int i = 0; i < count; i += 2) { + float x = localPoints[i]; + float y = localPoints[i + 1]; + if (path == null) { + path = new Path(); + path.moveTo(x, y); + mX = x; + mY = y; + } else { + float dx = Math.abs(x - mX); + float dy = Math.abs(y - mY); + if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) { + path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2); + mX = x; + mY = y; + } + } + } + + mCachedPath = path; + } + + /** + * Convert the stroke to a Path based on the number of points + * + * @param width the width of the bounding box of the target path + * @param height the height of the bounding box of the target path + * @param numSample the number of points needed + * + * @return the path + */ + public Path toPath(float width, float height, int numSample) { + final float[] pts = GestureUtilities.temporalSampling(this, numSample); + final RectF rect = boundingBox; + + GestureUtilities.translate(pts, -rect.left, -rect.top); + + float sx = width / rect.width(); + float sy = height / rect.height(); + float scale = sx > sy ? sy : sx; + GestureUtilities.scale(pts, scale, scale); + + float mX = 0; + float mY = 0; + + Path path = null; + + final int count = pts.length; + + for (int i = 0; i < count; i += 2) { + float x = pts[i]; + float y = pts[i + 1]; + if (path == null) { + path = new Path(); + path.moveTo(x, y); + mX = x; + mY = y; + } else { + float dx = Math.abs(x - mX); + float dy = Math.abs(y - mY); + if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) { + path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2); + mX = x; + mY = y; + } + } + } + + return path; + } + + void serialize(DataOutputStream out) throws IOException { + final float[] pts = points; + final long[] times = timestamps; + final int count = points.length; + + // Write number of points + out.writeInt(count / 2); + + for (int i = 0; i < count; i += 2) { + // Write X + out.writeFloat(pts[i]); + // Write Y + out.writeFloat(pts[i + 1]); + // Write timestamp + out.writeLong(times[i / 2]); + } + } + + static GestureStroke deserialize(DataInputStream in) throws IOException { + // Number of points + final int count = in.readInt(); + + final ArrayList<GesturePoint> points = new ArrayList<GesturePoint>(count); + for (int i = 0; i < count; i++) { + points.add(GesturePoint.deserialize(in)); + } + + return new GestureStroke(points); + } + + /** + * Invalidate the cached path that is used to render the stroke + */ + public void clearPath() { + if (mCachedPath != null) mCachedPath.rewind(); + } + + /** + * Compute an oriented bounding box of the stroke + * @return OrientedBoundingBox + */ + public OrientedBoundingBox computeOrientedBoundingBox() { + return GestureUtilities.computeOrientedBoundingBox(points); + } +} diff --git a/core/java/android/gesture/GestureUtilities.java b/core/java/android/gesture/GestureUtilities.java new file mode 100755 index 0000000..40d7029 --- /dev/null +++ b/core/java/android/gesture/GestureUtilities.java @@ -0,0 +1,475 @@ +/* + * Copyright (C) 2008-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.gesture; + +import android.graphics.RectF; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.io.Closeable; +import java.io.IOException; + +import static android.gesture.GestureConstants.*; + +final class GestureUtilities { + private static final int TEMPORAL_SAMPLING_RATE = 16; + + private GestureUtilities() { + } + + /** + * Closes the specified stream. + * + * @param stream The stream to close. + */ + static void closeStream(Closeable stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + Log.e(LOG_TAG, "Could not close stream", e); + } + } + } + + static float[] spatialSampling(Gesture gesture, int sampleMatrixDimension) { + final float targetPatchSize = sampleMatrixDimension - 1; // edge inclusive + float[] sample = new float[sampleMatrixDimension * sampleMatrixDimension]; + Arrays.fill(sample, 0); + + RectF rect = gesture.getBoundingBox(); + float sx = targetPatchSize / rect.width(); + float sy = targetPatchSize / rect.height(); + float scale = sx < sy ? sx : sy; + + float preDx = -rect.centerX(); + float preDy = -rect.centerY(); + float postDx = targetPatchSize / 2; + float postDy = targetPatchSize / 2; + + final ArrayList<GestureStroke> strokes = gesture.getStrokes(); + final int count = strokes.size(); + + int size; + float xpos; + float ypos; + + for (int index = 0; index < count; index++) { + final GestureStroke stroke = strokes.get(index); + float[] strokepoints = stroke.points; + size = strokepoints.length; + + final float[] pts = new float[size]; + + for (int i = 0; i < size; i += 2) { + pts[i] = (strokepoints[i] + preDx) * scale + postDx; + pts[i + 1] = (strokepoints[i + 1] + preDy) * scale + postDy; + } + + float segmentEndX = -1; + float segmentEndY = -1; + + for (int i = 0; i < size; i += 2) { + + float segmentStartX = pts[i] < 0 ? 0 : pts[i]; + float segmentStartY = pts[i + 1] < 0 ? 0 : pts[i + 1]; + + if (segmentStartX > targetPatchSize) { + segmentStartX = targetPatchSize; + } + + if (segmentStartY > targetPatchSize) { + segmentStartY = targetPatchSize; + } + + plot(segmentStartX, segmentStartY, sample, sampleMatrixDimension); + + if (segmentEndX != -1) { + // evaluate horizontally + if (segmentEndX > segmentStartX) { + xpos = (float) Math.ceil(segmentStartX); + float slope = (segmentEndY - segmentStartY) / (segmentEndX - segmentStartX); + while (xpos < segmentEndX) { + ypos = slope * (xpos - segmentStartX) + segmentStartY; + plot(xpos, ypos, sample, sampleMatrixDimension); + xpos++; + } + } else if (segmentEndX < segmentStartX){ + xpos = (float) Math.ceil(segmentEndX); + float slope = (segmentEndY - segmentStartY) / (segmentEndX - segmentStartX); + while (xpos < segmentStartX) { + ypos = slope * (xpos - segmentStartX) + segmentStartY; + plot(xpos, ypos, sample, sampleMatrixDimension); + xpos++; + } + } + + // evaluating vertically + if (segmentEndY > segmentStartY) { + ypos = (float) Math.ceil(segmentStartY); + float invertSlope = (segmentEndX - segmentStartX) / (segmentEndY - segmentStartY); + while (ypos < segmentEndY) { + xpos = invertSlope * (ypos - segmentStartY) + segmentStartX; + plot(xpos, ypos, sample, sampleMatrixDimension); + ypos++; + } + } else if (segmentEndY < segmentStartY) { + ypos = (float) Math.ceil(segmentEndY); + float invertSlope = (segmentEndX - segmentStartX) / (segmentEndY - segmentStartY); + while (ypos < segmentStartY) { + xpos = invertSlope * (ypos - segmentStartY) + segmentStartX; + plot(xpos, ypos, sample, sampleMatrixDimension); + ypos++; + } + } + } + + segmentEndX = segmentStartX; + segmentEndY = segmentStartY; + } + } + + + return sample; + } + + private static void plot(float x, float y, float[] sample, int sampleSize) { + x = x < 0 ? 0 : x; + y = y < 0 ? 0 : y; + int xFloor = (int) Math.floor(x); + int xCeiling = (int) Math.ceil(x); + int yFloor = (int) Math.floor(y); + int yCeiling = (int) Math.ceil(y); + + // if it's an integer + if (x == xFloor && y == yFloor) { + int index = yCeiling * sampleSize + xCeiling; + if (sample[index] < 1){ + sample[index] = 1; + } + } else { + double topLeft = Math.sqrt(Math.pow(xFloor - x, 2) + Math.pow(yFloor - y, 2)); + double topRight = Math.sqrt(Math.pow(xCeiling - x, 2) + Math.pow(yFloor - y, 2)); + double btmLeft = Math.sqrt(Math.pow(xFloor - x, 2) + Math.pow(yCeiling - y, 2)); + double btmRight = Math.sqrt(Math.pow(xCeiling - x, 2) + Math.pow(yCeiling - y, 2)); + double sum = topLeft + topRight + btmLeft + btmRight; + + double value = topLeft / sum; + int index = yFloor * sampleSize + xFloor; + if (value > sample[index]){ + sample[index] = (float) value; + } + + value = topRight / sum; + index = yFloor * sampleSize + xCeiling; + if (value > sample[index]){ + sample[index] = (float) value; + } + + value = btmLeft / sum; + index = yCeiling * sampleSize + xFloor; + if (value > sample[index]){ + sample[index] = (float) value; + } + + value = btmRight / sum; + index = yCeiling * sampleSize + xCeiling; + if (value > sample[index]){ + sample[index] = (float) value; + } + } + } + + /** + * Featurize a stroke into a vector of a given number of elements + * + * @param stroke + * @param sampleSize + * @return a float array + */ + static float[] temporalSampling(GestureStroke stroke, int sampleSize) { + final float increment = stroke.length / (sampleSize - 1); + int vectorLength = sampleSize * 2; + float[] vector = new float[vectorLength]; + float distanceSoFar = 0; + float[] pts = stroke.points; + float lstPointX = pts[0]; + float lstPointY = pts[1]; + int index = 0; + float currentPointX = Float.MIN_VALUE; + float currentPointY = Float.MIN_VALUE; + vector[index] = lstPointX; + index++; + vector[index] = lstPointY; + index++; + int i = 0; + int count = pts.length / 2; + while (i < count) { + if (currentPointX == Float.MIN_VALUE) { + i++; + if (i >= count) { + break; + } + currentPointX = pts[i * 2]; + currentPointY = pts[i * 2 + 1]; + } + float deltaX = currentPointX - lstPointX; + float deltaY = currentPointY - lstPointY; + float distance = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY); + if (distanceSoFar + distance >= increment) { + float ratio = (increment - distanceSoFar) / distance; + float nx = lstPointX + ratio * deltaX; + float ny = lstPointY + ratio * deltaY; + vector[index] = nx; + index++; + vector[index] = ny; + index++; + lstPointX = nx; + lstPointY = ny; + distanceSoFar = 0; + } else { + lstPointX = currentPointX; + lstPointY = currentPointY; + currentPointX = Float.MIN_VALUE; + currentPointY = Float.MIN_VALUE; + distanceSoFar += distance; + } + } + + for (i = index; i < vectorLength; i += 2) { + vector[i] = lstPointX; + vector[i + 1] = lstPointY; + } + return vector; + } + + /** + * Calculate the centroid + * + * @param points + * @return the centroid + */ + static float[] computeCentroid(float[] points) { + float centerX = 0; + float centerY = 0; + int count = points.length; + for (int i = 0; i < count; i++) { + centerX += points[i]; + i++; + centerY += points[i]; + } + float[] center = new float[2]; + center[0] = 2 * centerX / count; + center[1] = 2 * centerY / count; + + return center; + } + + /** + * calculate the variance-covariance matrix, treat each point as a sample + * + * @param points + * @return the covariance matrix + */ + private static double[][] computeCoVariance(float[] points) { + double[][] array = new double[2][2]; + array[0][0] = 0; + array[0][1] = 0; + array[1][0] = 0; + array[1][1] = 0; + int count = points.length; + for (int i = 0; i < count; i++) { + float x = points[i]; + i++; + float y = points[i]; + array[0][0] += x * x; + array[0][1] += x * y; + array[1][0] = array[0][1]; + array[1][1] += y * y; + } + array[0][0] /= (count / 2); + array[0][1] /= (count / 2); + array[1][0] /= (count / 2); + array[1][1] /= (count / 2); + + return array; + } + + static float computeTotalLength(float[] points) { + float sum = 0; + int count = points.length - 4; + for (int i = 0; i < count; i += 2) { + float dx = points[i + 2] - points[i]; + float dy = points[i + 3] - points[i + 1]; + sum += Math.sqrt(dx * dx + dy * dy); + } + return sum; + } + + static double computeStraightness(float[] points) { + float totalLen = computeTotalLength(points); + float dx = points[2] - points[0]; + float dy = points[3] - points[1]; + return Math.sqrt(dx * dx + dy * dy) / totalLen; + } + + static double computeStraightness(float[] points, float totalLen) { + float dx = points[2] - points[0]; + float dy = points[3] - points[1]; + return Math.sqrt(dx * dx + dy * dy) / totalLen; + } + + /** + * Calculate the squared Euclidean distance between two vectors + * + * @param vector1 + * @param vector2 + * @return the distance + */ + static double squaredEuclideanDistance(float[] vector1, float[] vector2) { + double squaredDistance = 0; + int size = vector1.length; + for (int i = 0; i < size; i++) { + float difference = vector1[i] - vector2[i]; + squaredDistance += difference * difference; + } + return squaredDistance / size; + } + + /** + * Calculate the cosine distance between two instances + * + * @param vector1 + * @param vector2 + * @return the distance between 0 and Math.PI + */ + static double cosineDistance(float[] vector1, float[] vector2) { + float sum = 0; + int len = vector1.length; + for (int i = 0; i < len; i++) { + sum += vector1[i] * vector2[i]; + } + return Math.acos(sum); + } + + static OrientedBoundingBox computeOrientedBoundingBox(ArrayList<GesturePoint> pts) { + GestureStroke stroke = new GestureStroke(pts); + float[] points = temporalSampling(stroke, TEMPORAL_SAMPLING_RATE); + return computeOrientedBoundingBox(points); + } + + static OrientedBoundingBox computeOrientedBoundingBox(float[] points) { + float[] meanVector = computeCentroid(points); + return computeOrientedBoundingBox(points, meanVector); + } + + static OrientedBoundingBox computeOrientedBoundingBox(float[] points, float[] centroid) { + translate(points, -centroid[0], -centroid[1]); + + double[][] array = computeCoVariance(points); + double[] targetVector = computeOrientation(array); + + float angle; + if (targetVector[0] == 0 && targetVector[1] == 0) { + angle = (float) -Math.PI/2; + } else { // -PI<alpha<PI + angle = (float) Math.atan2(targetVector[1], targetVector[0]); + rotate(points, -angle); + } + + float minx = Float.MAX_VALUE; + float miny = Float.MAX_VALUE; + float maxx = Float.MIN_VALUE; + float maxy = Float.MIN_VALUE; + int count = points.length; + for (int i = 0; i < count; i++) { + if (points[i] < minx) { + minx = points[i]; + } + if (points[i] > maxx) { + maxx = points[i]; + } + i++; + if (points[i] < miny) { + miny = points[i]; + } + if (points[i] > maxy) { + maxy = points[i]; + } + } + + return new OrientedBoundingBox((float) (angle * 180 / Math.PI), centroid[0], centroid[1], maxx - minx, maxy - miny); + } + + private static double[] computeOrientation(double[][] covarianceMatrix) { + double[] targetVector = new double[2]; + if (covarianceMatrix[0][1] == 0 || covarianceMatrix[1][0] == 0) { + targetVector[0] = 1; + targetVector[1] = 0; + } + + double a = -covarianceMatrix[0][0] - covarianceMatrix[1][1]; + double b = covarianceMatrix[0][0] * covarianceMatrix[1][1] - covarianceMatrix[0][1] + * covarianceMatrix[1][0]; + double value = a / 2; + double rightside = Math.sqrt(Math.pow(value, 2) - b); + double lambda1 = -value + rightside; + double lambda2 = -value - rightside; + if (lambda1 == lambda2) { + targetVector[0] = 0; + targetVector[1] = 0; + } else { + double lambda = lambda1 > lambda2 ? lambda1 : lambda2; + targetVector[0] = 1; + targetVector[1] = (lambda - covarianceMatrix[0][0]) / covarianceMatrix[0][1]; + } + return targetVector; + } + + + static float[] rotate(float[] points, double angle) { + double cos = Math.cos(angle); + double sin = Math.sin(angle); + int size = points.length; + for (int i = 0; i < size; i += 2) { + float x = (float) (points[i] * cos - points[i + 1] * sin); + float y = (float) (points[i] * sin + points[i + 1] * cos); + points[i] = x; + points[i + 1] = y; + } + return points; + } + + static float[] translate(float[] points, float dx, float dy) { + int size = points.length; + for (int i = 0; i < size; i += 2) { + points[i] += dx; + points[i + 1] += dy; + } + return points; + } + + static float[] scale(float[] points, float sx, float sy) { + int size = points.length; + for (int i = 0; i < size; i += 2) { + points[i] *= sx; + points[i + 1] *= sy; + } + return points; + } +} diff --git a/core/java/android/gesture/Instance.java b/core/java/android/gesture/Instance.java new file mode 100755 index 0000000..ef208ac --- /dev/null +++ b/core/java/android/gesture/Instance.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2008-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.gesture; + + +/** + * An instance represents a sample if the label is available or a query if the + * label is null. + */ +class Instance { + private static final int SEQUENCE_SAMPLE_SIZE = 16; + + private static final int PATCH_SAMPLE_SIZE = 16; + + private final static float[] ORIENTATIONS = { + 0, (float) (Math.PI / 4), (float) (Math.PI / 2), (float) (Math.PI * 3 / 4), + (float) Math.PI, -0, (float) (-Math.PI / 4), (float) (-Math.PI / 2), + (float) (-Math.PI * 3 / 4), (float) -Math.PI + }; + + // the feature vector + final float[] vector; + + // the label can be null + final String label; + + // the id of the instance + final long id; + + private Instance(long id, float[] sample, String sampleName) { + this.id = id; + vector = sample; + label = sampleName; + } + + private void normalize() { + float[] sample = vector; + float sum = 0; + + int size = sample.length; + for (int i = 0; i < size; i++) { + sum += sample[i] * sample[i]; + } + + float magnitude = (float)Math.sqrt(sum); + for (int i = 0; i < size; i++) { + sample[i] /= magnitude; + } + } + + /** + * create a learning instance for a single stroke gesture + * + * @param gesture + * @param label + * @return the instance + */ + static Instance createInstance(int sequenceType, int orientationType, Gesture gesture, String label) { + float[] pts; + Instance instance; + if (sequenceType == GestureStore.SEQUENCE_SENSITIVE) { + pts = temporalSampler(orientationType, gesture); + instance = new Instance(gesture.getID(), pts, label); + instance.normalize(); + } else { + pts = spatialSampler(gesture); + instance = new Instance(gesture.getID(), pts, label); + } + return instance; + } + + private static float[] spatialSampler(Gesture gesture) { + return GestureUtilities.spatialSampling(gesture, PATCH_SAMPLE_SIZE); + } + + private static float[] temporalSampler(int orientationType, Gesture gesture) { + float[] pts = GestureUtilities.temporalSampling(gesture.getStrokes().get(0), + SEQUENCE_SAMPLE_SIZE); + float[] center = GestureUtilities.computeCentroid(pts); + float orientation = (float)Math.atan2(pts[1] - center[1], pts[0] - center[0]); + + float adjustment = -orientation; + if (orientationType == GestureStore.ORIENTATION_SENSITIVE) { + int count = ORIENTATIONS.length; + for (int i = 0; i < count; i++) { + float delta = ORIENTATIONS[i] - orientation; + if (Math.abs(delta) < Math.abs(adjustment)) { + adjustment = delta; + } + } + } + + GestureUtilities.translate(pts, -center[0], -center[1]); + GestureUtilities.rotate(pts, adjustment); + + return pts; + } + +} diff --git a/core/java/android/gesture/InstanceLearner.java b/core/java/android/gesture/InstanceLearner.java new file mode 100644 index 0000000..b93b76f --- /dev/null +++ b/core/java/android/gesture/InstanceLearner.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2008-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.gesture; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.TreeMap; + +/** + * An implementation of an instance-based learner + */ + +class InstanceLearner extends Learner { + private static final Comparator<Prediction> sComparator = new Comparator<Prediction>() { + public int compare(Prediction object1, Prediction object2) { + double score1 = object1.score; + double score2 = object2.score; + if (score1 > score2) { + return -1; + } else if (score1 < score2) { + return 1; + } else { + return 0; + } + } + }; + + @Override + ArrayList<Prediction> classify(int sequenceType, float[] vector) { + ArrayList<Prediction> predictions = new ArrayList<Prediction>(); + ArrayList<Instance> instances = getInstances(); + int count = instances.size(); + TreeMap<String, Double> label2score = new TreeMap<String, Double>(); + for (int i = 0; i < count; i++) { + Instance sample = instances.get(i); + if (sample.vector.length != vector.length) { + continue; + } + double distance; + if (sequenceType == GestureStore.SEQUENCE_SENSITIVE) { + distance = GestureUtilities.cosineDistance(sample.vector, vector); + } else { + distance = GestureUtilities.squaredEuclideanDistance(sample.vector, vector); + } + double weight; + if (distance == 0) { + weight = Double.MAX_VALUE; + } else { + weight = 1 / distance; + } + Double score = label2score.get(sample.label); + if (score == null || weight > score) { + label2score.put(sample.label, weight); + } + } + +// double sum = 0; + for (String name : label2score.keySet()) { + double score = label2score.get(name); +// sum += score; + predictions.add(new Prediction(name, score)); + } + + // normalize +// for (Prediction prediction : predictions) { +// prediction.score /= sum; +// } + + Collections.sort(predictions, sComparator); + + return predictions; + } +} diff --git a/core/java/android/gesture/Learner.java b/core/java/android/gesture/Learner.java new file mode 100755 index 0000000..feacde5 --- /dev/null +++ b/core/java/android/gesture/Learner.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2008-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.gesture; + +import java.util.ArrayList; + +/** + * The abstract class of a gesture learner + */ +abstract class Learner { + private final ArrayList<Instance> mInstances = new ArrayList<Instance>(); + + /** + * Add an instance to the learner + * + * @param instance + */ + void addInstance(Instance instance) { + mInstances.add(instance); + } + + /** + * Retrieve all the instances + * + * @return instances + */ + ArrayList<Instance> getInstances() { + return mInstances; + } + + /** + * Remove an instance based on its id + * + * @param id + */ + void removeInstance(long id) { + ArrayList<Instance> instances = mInstances; + int count = instances.size(); + for (int i = 0; i < count; i++) { + Instance instance = instances.get(i); + if (id == instance.id) { + instances.remove(instance); + return; + } + } + } + + /** + * Remove all the instances of a category + * + * @param name the category name + */ + void removeInstances(String name) { + final ArrayList<Instance> toDelete = new ArrayList<Instance>(); + final ArrayList<Instance> instances = mInstances; + final int count = instances.size(); + + for (int i = 0; i < count; i++) { + final Instance instance = instances.get(i); + // the label can be null, as specified in Instance + if ((instance.label == null && name == null) || instance.label.equals(name)) { + toDelete.add(instance); + } + } + instances.removeAll(toDelete); + } + + abstract ArrayList<Prediction> classify(int gestureType, float[] vector); +} diff --git a/core/java/android/gesture/OrientedBoundingBox.java b/core/java/android/gesture/OrientedBoundingBox.java new file mode 100644 index 0000000..f1335ee --- /dev/null +++ b/core/java/android/gesture/OrientedBoundingBox.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2008-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.gesture; + +import android.graphics.Matrix; +import android.graphics.Path; + +/** + * An oriented bounding box + */ +public class OrientedBoundingBox { + public final float squareness; + + public final float width; + public final float height; + + public final float orientation; + + public final float centerX; + public final float centerY; + + OrientedBoundingBox(float angle, float cx, float cy, float w, float h) { + orientation = angle; + width = w; + height = h; + centerX = cx; + centerY = cy; + float ratio = w / h; + if (ratio > 1) { + squareness = 1 / ratio; + } else { + squareness = ratio; + } + } + + /** + * Currently used for debugging purpose only. + * + * @hide + */ + public Path toPath() { + Path path = new Path(); + float[] point = new float[2]; + point[0] = -width / 2; + point[1] = height / 2; + Matrix matrix = new Matrix(); + matrix.setRotate(orientation); + matrix.postTranslate(centerX, centerY); + matrix.mapPoints(point); + path.moveTo(point[0], point[1]); + + point[0] = -width / 2; + point[1] = -height / 2; + matrix.mapPoints(point); + path.lineTo(point[0], point[1]); + + point[0] = width / 2; + point[1] = -height / 2; + matrix.mapPoints(point); + path.lineTo(point[0], point[1]); + + point[0] = width / 2; + point[1] = height / 2; + matrix.mapPoints(point); + path.lineTo(point[0], point[1]); + + path.close(); + + return path; + } +} diff --git a/core/java/android/gesture/Prediction.java b/core/java/android/gesture/Prediction.java new file mode 100755 index 0000000..ce6ad57 --- /dev/null +++ b/core/java/android/gesture/Prediction.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2008-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.gesture; + +public class Prediction { + public final String name; + + public double score; + + Prediction(String label, double predictionScore) { + name = label; + score = predictionScore; + } + + @Override + public String toString() { + return name; + } +} diff --git a/core/java/android/gesture/package.html b/core/java/android/gesture/package.html new file mode 100644 index 0000000..a54a017 --- /dev/null +++ b/core/java/android/gesture/package.html @@ -0,0 +1,5 @@ +<HTML> +<BODY> +@hide +</BODY> +</HTML> diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index ca579b6..091bc17 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -39,13 +39,16 @@ import android.os.Message; public class Camera { private static final String TAG = "Camera"; - // These match the enum in libs/android_runtime/android_hardware_Camera.cpp - private static final int SHUTTER_CALLBACK = 0; - private static final int RAW_PICTURE_CALLBACK = 1; - private static final int JPEG_PICTURE_CALLBACK = 2; - private static final int PREVIEW_CALLBACK = 3; - private static final int AUTOFOCUS_CALLBACK = 4; - private static final int ERROR_CALLBACK = 5; + // These match the enums in frameworks/base/include/ui/Camera.h + private static final int CAMERA_MSG_ERROR = 0; + private static final int CAMERA_MSG_SHUTTER = 1; + private static final int CAMERA_MSG_FOCUS = 2; + private static final int CAMERA_MSG_ZOOM = 3; + private static final int CAMERA_MSG_PREVIEW_FRAME = 4; + private static final int CAMERA_MSG_VIDEO_FRAME = 5; + private static final int CAMERA_MSG_POSTVIEW_FRAME = 6; + private static final int CAMERA_MSG_RAW_IMAGE = 7; + private static final int CAMERA_MSG_COMPRESSED_IMAGE = 8; private int mNativeContext; // accessed by native methods private EventHandler mEventHandler; @@ -152,7 +155,11 @@ public class Camera { * @throws IOException if the method fails. */ public final void setPreviewDisplay(SurfaceHolder holder) throws IOException { - setPreviewDisplay(holder.getSurface()); + if (holder != null) { + setPreviewDisplay(holder.getSurface()); + } else { + setPreviewDisplay((Surface)null); + } } private native final void setPreviewDisplay(Surface surface); @@ -231,22 +238,23 @@ public class Camera { @Override public void handleMessage(Message msg) { switch(msg.what) { - case SHUTTER_CALLBACK: + case CAMERA_MSG_SHUTTER: if (mShutterCallback != null) { mShutterCallback.onShutter(); } return; - case RAW_PICTURE_CALLBACK: + + case CAMERA_MSG_RAW_IMAGE: if (mRawImageCallback != null) mRawImageCallback.onPictureTaken((byte[])msg.obj, mCamera); return; - case JPEG_PICTURE_CALLBACK: + case CAMERA_MSG_COMPRESSED_IMAGE: if (mJpegCallback != null) mJpegCallback.onPictureTaken((byte[])msg.obj, mCamera); return; - case PREVIEW_CALLBACK: + case CAMERA_MSG_PREVIEW_FRAME: if (mPreviewCallback != null) { mPreviewCallback.onPreviewFrame((byte[])msg.obj, mCamera); if (mOneShot) { @@ -255,12 +263,12 @@ public class Camera { } return; - case AUTOFOCUS_CALLBACK: + case CAMERA_MSG_FOCUS: if (mAutoFocusCallback != null) mAutoFocusCallback.onAutoFocus(msg.arg1 == 0 ? false : true, mCamera); return; - case ERROR_CALLBACK: + case CAMERA_MSG_ERROR : Log.e(TAG, "Error " + msg.arg1); if (mErrorCallback != null) mErrorCallback.onError(msg.arg1, mCamera); @@ -363,7 +371,7 @@ public class Camera { } private native final void native_takePicture(); - // These match the enum in libs/android_runtime/android_hardware_Camera.cpp + // These match the enum in include/ui/Camera.h /** Unspecified camerar error. @see #ErrorCallback */ public static final int CAMERA_ERROR_UNKNOWN = 1; /** Media server died. In this case, the application must release the diff --git a/core/java/android/hardware/ISensorService.aidl b/core/java/android/hardware/ISensorService.aidl index 04af2ae..67180bd 100644 --- a/core/java/android/hardware/ISensorService.aidl +++ b/core/java/android/hardware/ISensorService.aidl @@ -17,13 +17,13 @@ package android.hardware; -import android.os.ParcelFileDescriptor; +import android.os.Bundle; /** * {@hide} */ interface ISensorService { - ParcelFileDescriptor getDataChanel(); + Bundle getDataChannel(); boolean enableSensor(IBinder listener, String name, int sensor, int enable); } diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index 67df23b..bf945ec 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -18,7 +18,9 @@ package android.hardware; import android.content.Context; import android.os.Binder; +import android.os.Bundle; import android.os.Looper; +import android.os.Parcelable; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; @@ -280,8 +282,8 @@ public class SensorManager void startLocked(ISensorService service) { try { if (mThread == null) { - ParcelFileDescriptor fd = service.getDataChanel(); - mThread = new Thread(new SensorThreadRunnable(fd), + Bundle dataChannel = service.getDataChannel(); + mThread = new Thread(new SensorThreadRunnable(dataChannel), SensorThread.class.getName()); mThread.start(); } @@ -291,10 +293,52 @@ public class SensorManager } private class SensorThreadRunnable implements Runnable { - private ParcelFileDescriptor mSensorDataFd; - SensorThreadRunnable(ParcelFileDescriptor fd) { - mSensorDataFd = fd; + private Bundle mDataChannel; + SensorThreadRunnable(Bundle dataChannel) { + mDataChannel = dataChannel; } + + private boolean open() { + if (mDataChannel == null) { + Log.e(TAG, "mDataChannel == NULL, exiting"); + synchronized (sListeners) { + mThread = null; + } + return false; + } + + // this thread is guaranteed to be unique + Parcelable[] pfds = mDataChannel.getParcelableArray("fds"); + FileDescriptor[] fds; + if (pfds != null) { + int length = pfds.length; + fds = new FileDescriptor[length]; + for (int i = 0; i < length; i++) { + ParcelFileDescriptor pfd = (ParcelFileDescriptor)pfds[i]; + fds[i] = pfd.getFileDescriptor(); + } + } else { + fds = null; + } + int[] ints = mDataChannel.getIntArray("ints"); + sensors_data_open(fds, ints); + if (pfds != null) { + try { + // close our copies of the file descriptors, + // since we are just passing these to the JNI code and not using them here. + for (int i = pfds.length - 1; i >= 0; i--) { + ParcelFileDescriptor pfd = (ParcelFileDescriptor)pfds[i]; + pfd.close(); + } + } catch (IOException e) { + // *shrug* + Log.e(TAG, "IOException: ", e); + } + } + mDataChannel = null; + return true; + } + public void run() { //Log.d(TAG, "entering main sensor thread"); final float[] values = new float[3]; @@ -302,23 +346,9 @@ public class SensorManager final long timestamp[] = new long[1]; Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); - if (mSensorDataFd == null) { - Log.e(TAG, "mSensorDataFd == NULL, exiting"); - synchronized (sListeners) { - mThread = null; - } + if (!open()) { return; } - // this thread is guaranteed to be unique - sensors_data_open(mSensorDataFd.getFileDescriptor()); - try { - mSensorDataFd.close(); - } catch (IOException e) { - // *shrug* - Log.e(TAG, "IOException: ", e); - } - mSensorDataFd = null; - while (true) { // wait for an event @@ -1469,7 +1499,7 @@ public class SensorManager // Used within this module from outside SensorManager, don't make private static native int sensors_data_init(); static native int sensors_data_uninit(); - static native int sensors_data_open(FileDescriptor fd); + static native int sensors_data_open(FileDescriptor[] fds, int[] ints); static native int sensors_data_close(); static native int sensors_data_poll(float[] values, int[] status, long[] timestamp); } diff --git a/core/java/android/net/http/RequestHandle.java b/core/java/android/net/http/RequestHandle.java index c4ee5b0..6a97951 100644 --- a/core/java/android/net/http/RequestHandle.java +++ b/core/java/android/net/http/RequestHandle.java @@ -159,11 +159,11 @@ public class RequestHandle { e.printStackTrace(); } - // update the "cookie" header based on the redirected url - mHeaders.remove("cookie"); + // update the "Cookie" header based on the redirected url + mHeaders.remove("Cookie"); String cookie = CookieManager.getInstance().getCookie(mUri); if (cookie != null && cookie.length() > 0) { - mHeaders.put("cookie", cookie); + mHeaders.put("Cookie", cookie); } if ((statusCode == 302 || statusCode == 303) && mMethod.equals("POST")) { diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index 6c13582..abfb274 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -127,12 +127,12 @@ import java.util.concurrent.atomic.AtomicInteger; public abstract class AsyncTask<Params, Progress, Result> { private static final String LOG_TAG = "AsyncTask"; - private static final int CORE_POOL_SIZE = 1; - private static final int MAXIMUM_POOL_SIZE = 10; + private static final int CORE_POOL_SIZE = 5; + private static final int MAXIMUM_POOL_SIZE = 128; private static final int KEEP_ALIVE = 10; private static final BlockingQueue<Runnable> sWorkQueue = - new LinkedBlockingQueue<Runnable>(MAXIMUM_POOL_SIZE); + new LinkedBlockingQueue<Runnable>(10); private static final ThreadFactory sThreadFactory = new ThreadFactory() { private final AtomicInteger mCount = new AtomicInteger(1); diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 8a0fd58..528def5 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -69,6 +69,20 @@ public abstract class BatteryStats implements Parcelable { public static final int WIFI_MULTICAST_ENABLED = 7; /** + * A constant indicating an audio turn on timer + * + * {@hide} + */ + public static final int AUDIO_TURNED_ON = 7; + + /** + * A constant indicating a video turn on timer + * + * {@hide} + */ + public static final int VIDEO_TURNED_ON = 8; + + /** * Include all of the data in the stats, including previously saved data. */ public static final int STATS_TOTAL = 0; @@ -164,7 +178,7 @@ public abstract class BatteryStats implements Parcelable { * @return a time in microseconds */ public abstract long getTotalTimeLocked(long batteryRealtime, int which); - + /** * Temporary for debugging. */ @@ -234,11 +248,17 @@ public abstract class BatteryStats implements Parcelable { public abstract void noteScanWifiLockReleasedLocked(); public abstract void noteWifiMulticastEnabledLocked(); public abstract void noteWifiMulticastDisabledLocked(); + public abstract void noteAudioTurnedOnLocked(); + public abstract void noteAudioTurnedOffLocked(); + public abstract void noteVideoTurnedOnLocked(); + public abstract void noteVideoTurnedOffLocked(); public abstract long getWifiTurnedOnTime(long batteryRealtime, int which); public abstract long getFullWifiLockTime(long batteryRealtime, int which); public abstract long getScanWifiLockTime(long batteryRealtime, int which); public abstract long getWifiMulticastTime(long batteryRealtime, int which); + public abstract long getAudioTurnedOnTime(long batteryRealtime, int which); + public abstract long getVideoTurnedOnTime(long batteryRealtime, int which); /** * Note that these must match the constants in android.os.LocalPowerManager. @@ -287,6 +307,13 @@ public abstract class BatteryStats implements Parcelable { * @param which one of STATS_TOTAL, STATS_LAST, or STATS_CURRENT. */ public abstract int getStarts(int which); + + /** + * Returns the cpu time spent in microseconds while the process was in the foreground. + * @param which one of STATS_TOTAL, STATS_LAST, STATS_CURRENT or STATS_UNPLUGGED + * @return foreground cpu time in microseconds + */ + public abstract long getForegroundTime(int which); } /** @@ -344,7 +371,7 @@ public abstract class BatteryStats implements Parcelable { public abstract int getStartCount(); /** - * Returns the time in milliseconds that the screen has been on while the device was + * Returns the time in microseconds that the screen has been on while the device was * running on battery. * * {@hide} @@ -364,7 +391,7 @@ public abstract class BatteryStats implements Parcelable { public static final int NUM_SCREEN_BRIGHTNESS_BINS = 5; /** - * Returns the time in milliseconds that the screen has been on with + * Returns the time in microseconds that the screen has been on with * the given brightness * * {@hide} @@ -375,7 +402,7 @@ public abstract class BatteryStats implements Parcelable { public abstract int getInputEventCount(int which); /** - * Returns the time in milliseconds that the phone has been on while the device was + * Returns the time in microseconds that the phone has been on while the device was * running on battery. * * {@hide} @@ -395,7 +422,7 @@ public abstract class BatteryStats implements Parcelable { public static final int NUM_SIGNAL_STRENGTH_BINS = 5; /** - * Returns the time in milliseconds that the phone has been running with + * Returns the time in microseconds that the phone has been running with * the given signal strength. * * {@hide} @@ -423,7 +450,7 @@ public abstract class BatteryStats implements Parcelable { public static final int NUM_DATA_CONNECTION_TYPES = 5; /** - * Returns the time in milliseconds that the phone has been running with + * Returns the time in microseconds that the phone has been running with * the given data connection. * * {@hide} @@ -440,7 +467,7 @@ public abstract class BatteryStats implements Parcelable { public abstract int getPhoneDataConnectionCount(int dataType, int which); /** - * Returns the time in milliseconds that wifi has been on while the device was + * Returns the time in microseconds that wifi has been on while the device was * running on battery. * * {@hide} @@ -448,7 +475,7 @@ public abstract class BatteryStats implements Parcelable { public abstract long getWifiOnTime(long batteryRealtime, int which); /** - * Returns the time in milliseconds that wifi has been on and the driver has + * Returns the time in microseconds that wifi has been on and the driver has * been in the running state while the device was running on battery. * * {@hide} @@ -456,7 +483,7 @@ public abstract class BatteryStats implements Parcelable { public abstract long getWifiRunningTime(long batteryRealtime, int which); /** - * Returns the time in milliseconds that bluetooth has been on while the device was + * Returns the time in microseconds that bluetooth has been on while the device was * running on battery. * * {@hide} diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 5487c54..830b0bd 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -38,6 +38,12 @@ public class Build { /** The name of the underlying board, like "goldfish". */ public static final String BOARD = getString("ro.product.board"); + /** The name of the instruction set (CPU type + ABI convention) of native code. */ + public static final String CPU_ABI = getString("ro.product.cpu.abi"); + + /** The manufacturer of the product/hardware. */ + public static final String MANUFACTURER = getString("ro.product.manufacturer"); + /** The brand (e.g., carrier) the software is customized for, if any. */ public static final String BRAND = getString("ro.product.brand"); @@ -87,6 +93,12 @@ public class Build { */ public static class VERSION_CODES { /** + * Magic version number for a current development build, which has + * not yet turned into an official release. + */ + public static final int CUR_DEVELOPMENT = 10000; + + /** * October 2008: The original, first, version of Android. Yay! */ public static final int BASE = 1; @@ -98,6 +110,19 @@ public class Build { * May 2009: Android 1.5. */ public static final int CUPCAKE = 3; + /** + * Current work on "Donut" development branch. + * + * <p>Applications targeting this or a later release will get these + * new changes in behavior:</p> + * <ul> + * <li> They must explicitly request the + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} permission to be + * able to modify the contents of the SD card. (Apps targeting + * earlier versions will always request the permission.) + * </ul> + */ + public static final int DONUT = CUR_DEVELOPMENT; } /** The type of build, like "user" or "eng". */ diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index b669fa2..a91655f 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -78,6 +78,10 @@ public final class Bundle implements Parcelable, Cloneable { readFromParcel(parcelledData); } + /* package */ Bundle(Parcel parcelledData, int length) { + readFromParcelInner(parcelledData, length); + } + /** * Constructs a new, empty Bundle that uses a specific ClassLoader for * instantiating Parcelable and Serializable objects. @@ -155,13 +159,14 @@ public final class Bundle implements Parcelable, Cloneable { return; } - mParcelledData.setDataPosition(0); - Bundle b = mParcelledData.readBundleUnpacked(mClassLoader); - mMap = b.mMap; - - mHasFds = mParcelledData.hasFileDescriptors(); - mFdsKnown = true; - + int N = mParcelledData.readInt(); + if (N < 0) { + return; + } + if (mMap == null) { + mMap = new HashMap<String, Object>(); + } + mParcelledData.readMapInternal(mMap, N, mClassLoader); mParcelledData.recycle(); mParcelledData = null; } @@ -1427,7 +1432,25 @@ public final class Bundle implements Parcelable, Cloneable { * @param parcel The parcel to copy this bundle to. */ public void writeToParcel(Parcel parcel, int flags) { - parcel.writeBundle(this); + if (mParcelledData != null) { + int length = mParcelledData.dataSize(); + parcel.writeInt(length); + parcel.writeInt(0x4C444E42); // 'B' 'N' 'D' 'L' + parcel.appendFrom(mParcelledData, 0, length); + } else { + parcel.writeInt(-1); // dummy, will hold length + parcel.writeInt(0x4C444E42); // 'B' 'N' 'D' 'L' + + int oldPos = parcel.dataPosition(); + parcel.writeMapInternal(mMap); + int newPos = parcel.dataPosition(); + + // Backpatch length + parcel.setDataPosition(oldPos - 8); + int length = newPos - oldPos; + parcel.writeInt(length); + parcel.setDataPosition(newPos); + } } /** @@ -1436,8 +1459,33 @@ public final class Bundle implements Parcelable, Cloneable { * @param parcel The parcel to overwrite this bundle from. */ public void readFromParcel(Parcel parcel) { - mParcelledData = parcel; - mHasFds = mParcelledData.hasFileDescriptors(); + int length = parcel.readInt(); + if (length < 0) { + throw new RuntimeException("Bad length in parcel: " + length); + } + readFromParcelInner(parcel, length); + } + + void readFromParcelInner(Parcel parcel, int length) { + int magic = parcel.readInt(); + if (magic != 0x4C444E42) { + //noinspection ThrowableInstanceNeverThrown + String st = Log.getStackTraceString(new RuntimeException()); + Log.e("Bundle", "readBundle: bad magic number"); + Log.e("Bundle", "readBundle: trace = " + st); + } + + // Advance within this Parcel + int offset = parcel.dataPosition(); + parcel.setDataPosition(offset + length); + + Parcel p = Parcel.obtain(); + p.setDataPosition(0); + p.appendFrom(parcel, offset, length); + p.setDataPosition(0); + + mParcelledData = p; + mHasFds = p.hasFileDescriptors(); mFdsKnown = true; } diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 8fcb4d7..d40ea6b 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -21,6 +21,7 @@ import com.android.internal.util.TypedProperties; import android.util.Config; import android.util.Log; +import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; @@ -378,6 +379,20 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo } /** + * Like startMethodTracing(String, int, int), but taking an already-opened + * FileDescriptor in which the trace is written. The file name is also + * supplied simply for logging. Makes a dup of the file descriptor. + * + * Not exposed in the SDK unless we are really comfortable with supporting + * this and find it would be useful. + * @hide + */ + public static void startMethodTracing(String traceName, FileDescriptor fd, + int bufferSize, int flags) { + VMDebug.startMethodTracing(traceName, fd, bufferSize, flags); + } + + /** * Determine whether method tracing is currently active. * @hide */ diff --git a/core/java/android/os/MemoryFile.java b/core/java/android/os/MemoryFile.java index 76e4f47..c14925c 100644 --- a/core/java/android/os/MemoryFile.java +++ b/core/java/android/os/MemoryFile.java @@ -18,6 +18,7 @@ package android.os; import android.util.Log; +import java.io.FileDescriptor; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -35,48 +36,120 @@ import java.io.OutputStream; public class MemoryFile { private static String TAG = "MemoryFile"; - - // returns fd - private native int native_open(String name, int length); + + // mmap(2) protection flags from <sys/mman.h> + private static final int PROT_READ = 0x1; + private static final int PROT_WRITE = 0x2; + + private static native FileDescriptor native_open(String name, int length) throws IOException; // returns memory address for ashmem region - private native int native_mmap(int fd, int length); - private native void native_close(int fd); - private native int native_read(int fd, int address, byte[] buffer, - int srcOffset, int destOffset, int count, boolean isUnpinned); - private native void native_write(int fd, int address, byte[] buffer, - int srcOffset, int destOffset, int count, boolean isUnpinned); - private native void native_pin(int fd, boolean pin); - - private int mFD; // ashmem file descriptor + private static native int native_mmap(FileDescriptor fd, int length, int mode) + throws IOException; + private static native void native_munmap(int addr, int length) throws IOException; + private static native void native_close(FileDescriptor fd); + private static native int native_read(FileDescriptor fd, int address, byte[] buffer, + int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException; + private static native void native_write(FileDescriptor fd, int address, byte[] buffer, + int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException; + private static native void native_pin(FileDescriptor fd, boolean pin) throws IOException; + private static native boolean native_is_ashmem_region(FileDescriptor fd) throws IOException; + + private FileDescriptor mFD; // ashmem file descriptor private int mAddress; // address of ashmem memory private int mLength; // total length of our ashmem region private boolean mAllowPurging = false; // true if our ashmem region is unpinned + private final boolean mOwnsRegion; // false if this is a ref to an existing ashmem region /** - * MemoryFile constructor. + * Allocates a new ashmem region. The region is initially not purgable. * * @param name optional name for the file (can be null). * @param length of the memory file in bytes. + * @throws IOException if the memory file could not be created. */ - public MemoryFile(String name, int length) { + public MemoryFile(String name, int length) throws IOException { mLength = length; mFD = native_open(name, length); - mAddress = native_mmap(mFD, length); + mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE); + mOwnsRegion = true; } /** - * Closes and releases all resources for the memory file. + * Creates a reference to an existing memory file. Changes to the original file + * will be available through this reference. + * Calls to {@link #allowPurging(boolean)} on the returned MemoryFile will fail. + * + * @param fd File descriptor for an existing memory file, as returned by + * {@link #getFileDescriptor()}. This file descriptor will be closed + * by {@link #close()}. + * @param length Length of the memory file in bytes. + * @param mode File mode. Currently only "r" for read-only access is supported. + * @throws NullPointerException if <code>fd</code> is null. + * @throws IOException If <code>fd</code> does not refer to an existing memory file, + * or if the file mode of the existing memory file is more restrictive + * than <code>mode</code>. + * + * @hide + */ + public MemoryFile(FileDescriptor fd, int length, String mode) throws IOException { + if (fd == null) { + throw new NullPointerException("File descriptor is null."); + } + if (!isMemoryFile(fd)) { + throw new IllegalArgumentException("Not a memory file."); + } + mLength = length; + mFD = fd; + mAddress = native_mmap(mFD, length, modeToProt(mode)); + mOwnsRegion = false; + } + + /** + * Closes the memory file. If there are no other open references to the memory + * file, it will be deleted. */ public void close() { - if (mFD > 0) { + deactivate(); + if (!isClosed()) { native_close(mFD); - mFD = 0; } } + /** + * Unmaps the memory file from the process's memory space, but does not close it. + * After this method has been called, read and write operations through this object + * will fail, but {@link #getFileDescriptor()} will still return a valid file descriptor. + * + * @hide + */ + public void deactivate() { + if (!isDeactivated()) { + try { + native_munmap(mAddress, mLength); + mAddress = 0; + } catch (IOException ex) { + Log.e(TAG, ex.toString()); + } + } + } + + /** + * Checks whether the memory file has been deactivated. + */ + private boolean isDeactivated() { + return mAddress == 0; + } + + /** + * Checks whether the memory file has been closed. + */ + private boolean isClosed() { + return !mFD.valid(); + } + @Override protected void finalize() { - if (mFD > 0) { + if (!isClosed()) { Log.e(TAG, "MemoryFile.finalize() called while ashmem still open"); close(); } @@ -108,6 +181,9 @@ public class MemoryFile * @return previous value of allowPurging */ synchronized public boolean allowPurging(boolean allowPurging) throws IOException { + if (!mOwnsRegion) { + throw new IOException("Only the owner can make ashmem regions purgable."); + } boolean oldValue = mAllowPurging; if (oldValue != allowPurging) { native_pin(mFD, !allowPurging); @@ -131,7 +207,6 @@ public class MemoryFile @return OutputStream */ public OutputStream getOutputStream() { - return new MemoryOutputStream(); } @@ -144,9 +219,13 @@ public class MemoryFile * @param destOffset offset into the byte array buffer to read into. * @param count number of bytes to read. * @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) throws IOException { + if (isDeactivated()) { + throw new IOException("Can't read from deactivated memory file."); + } if (destOffset < 0 || destOffset > buffer.length || count < 0 || count > buffer.length - destOffset || srcOffset < 0 || srcOffset > mLength @@ -164,9 +243,13 @@ public class MemoryFile * @param srcOffset offset into the byte array buffer to write from. * @param destOffset offset into the memory file to write to. * @param count number of bytes to write. + * @throws IOException if the memory file has been purged or deactivated. */ public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count) throws IOException { + if (isDeactivated()) { + throw new IOException("Can't write to deactivated memory file."); + } if (srcOffset < 0 || srcOffset > buffer.length || count < 0 || count > buffer.length - srcOffset || destOffset < 0 || destOffset > mLength @@ -176,6 +259,64 @@ public class MemoryFile native_write(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging); } + /** + * Gets a ParcelFileDescriptor for the memory file. See {@link #getFileDescriptor()} + * for caveats. This must be here to allow classes outside <code>android.os</code< to + * make ParcelFileDescriptors from MemoryFiles, as + * {@link ParcelFileDescriptor#ParcelFileDescriptor(FileDescriptor)} is package private. + * + * + * @return The file descriptor owned by this memory file object. + * The file descriptor is not duplicated. + * @throws IOException If the memory file has been closed. + * + * @hide + */ + public ParcelFileDescriptor getParcelFileDescriptor() throws IOException { + return new ParcelFileDescriptor(getFileDescriptor()); + } + + /** + * Gets a FileDescriptor for the memory file. Note that this file descriptor + * is only safe to pass to {@link #MemoryFile(FileDescriptor,int)}). It + * should not be used with file descriptor operations that expect a file descriptor + * for a normal file. + * + * The returned file descriptor is not duplicated. + * + * @throws IOException If the memory file has been closed. + * + * @hide + */ + public FileDescriptor getFileDescriptor() throws IOException { + return mFD; + } + + /** + * Checks whether the given file descriptor refers to a memory file. + * + * @throws IOException If <code>fd</code> is not a valid file descriptor. + * + * @hide + */ + public static boolean isMemoryFile(FileDescriptor fd) throws IOException { + return native_is_ashmem_region(fd); + } + + /** + * Converts a file mode string to a <code>prot</code> value as expected by + * native_mmap(). + * + * @throws IllegalArgumentException if the file mode is invalid. + */ + private static int modeToProt(String mode) { + if ("r".equals(mode)) { + return PROT_READ; + } else { + throw new IllegalArgumentException("Unsupported file mode: '" + mode + "'"); + } + } + private class MemoryInputStream extends InputStream { private int mMark = 0; @@ -212,13 +353,22 @@ public class MemoryFile } int result = read(mSingleByte, 0, 1); if (result != 1) { - throw new IOException("read() failed"); + return -1; } return mSingleByte[0]; } @Override public int read(byte buffer[], int offset, int count) throws IOException { + if (offset < 0 || count < 0 || offset + count > buffer.length) { + // readBytes() also does this check, but we need to do it before + // changing count. + throw new IndexOutOfBoundsException(); + } + count = Math.min(count, available()); + if (count < 1) { + return -1; + } int result = readBytes(buffer, mOffset, offset, count); if (result > 0) { mOffset += result; diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 9a71f6e..6cfccee 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -457,7 +457,7 @@ public final class Parcel { * Flatten a Map into the parcel at the current dataPosition(), * growing dataCapacity() if needed. The Map keys must be String objects. */ - private void writeMapInternal(Map<String,Object> val) { + /* package */ void writeMapInternal(Map<String,Object> val) { if (val == null) { writeInt(-1); return; @@ -480,23 +480,7 @@ public final class Parcel { return; } - if (val.mParcelledData != null) { - int length = val.mParcelledData.dataSize(); - appendFrom(val.mParcelledData, 0, length); - } else { - writeInt(-1); // dummy, will hold length - int oldPos = dataPosition(); - writeInt(0x4C444E42); // 'B' 'N' 'D' 'L' - - writeMapInternal(val.mMap); - int newPos = dataPosition(); - - // Backpatch length - setDataPosition(oldPos - 4); - int length = newPos - oldPos; - writeInt(length); - setDataPosition(newPos); - } + val.writeToParcel(this, 0); } /** @@ -1352,27 +1336,12 @@ public final class Parcel { * Returns null if the previously written Bundle object was null. */ public final Bundle readBundle(ClassLoader loader) { - int offset = dataPosition(); int length = readInt(); if (length < 0) { return null; } - int magic = readInt(); - if (magic != 0x4C444E42) { - //noinspection ThrowableInstanceNeverThrown - String st = Log.getStackTraceString(new RuntimeException()); - Log.e("Bundle", "readBundle: bad magic number"); - Log.e("Bundle", "readBundle: trace = " + st); - } - - // Advance within this Parcel - setDataPosition(offset + length + 4); - - Parcel p = new Parcel(0); - p.setDataPosition(0); - p.appendFrom(this, offset, length + 4); - p.setDataPosition(0); - final Bundle bundle = new Bundle(p); + + final Bundle bundle = new Bundle(this, length); if (loader != null) { bundle.setClassLoader(loader); } @@ -1380,33 +1349,6 @@ public final class Parcel { } /** - * Read and return a new Bundle object from the parcel at the current - * dataPosition(). Returns null if the previously written Bundle object was - * null. The returned bundle will have its contents fully unpacked using - * the given ClassLoader. - */ - /* package */ Bundle readBundleUnpacked(ClassLoader loader) { - int length = readInt(); - if (length == -1) { - return null; - } - int magic = readInt(); - if (magic != 0x4C444E42) { - //noinspection ThrowableInstanceNeverThrown - String st = Log.getStackTraceString(new RuntimeException()); - Log.e("Bundle", "readBundleUnpacked: bad magic number"); - Log.e("Bundle", "readBundleUnpacked: trace = " + st); - } - Bundle m = new Bundle(loader); - int N = readInt(); - if (N < 0) { - return null; - } - readMapInternal(m.mMap, N, loader); - return m; - } - - /** * Read and return a byte[] object from the parcel. */ public final native byte[] createByteArray(); @@ -1998,7 +1940,7 @@ public final class Parcel { private native void init(int obj); private native void destroy(); - private void readMapInternal(Map outVal, int N, + /* package */ void readMapInternal(Map outVal, int N, ClassLoader loader) { while (N > 0) { Object key = readValue(loader); diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 30acef9..1214abc 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -573,7 +573,21 @@ public class Process { * directly to a gid. */ public static final native int getGidForName(String name); - + + /** + * Returns a uid for a currently running process. + * @param pid the process id + * @return the uid of the process, or -1 if the process is not running. + * @hide pending API council review + */ + public static final int getUidForPid(int pid) { + String[] procStatusLabels = { "Uid:" }; + 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. * @@ -604,6 +618,20 @@ public class Process { */ public static final native void setThreadGroup(int tid, int group) throws IllegalArgumentException, SecurityException; + /** + * Sets the scheduling group for a process and all child threads + * @hide + * @param pid The indentifier of the process to change. + * @param group The target group for this process. + * + * @throws IllegalArgumentException Throws IllegalArgumentException if + * <var>tid</var> does not exist. + * @throws SecurityException Throws SecurityException if your process does + * not have permission to modify the given thread, or to use the given + * priority. + */ + public static final native void setProcessGroup(int pid, int group) + throws IllegalArgumentException, SecurityException; /** * Set the priority of the calling thread, based on Linux priorities. See diff --git a/core/java/android/pim/EventRecurrence.java b/core/java/android/pim/EventRecurrence.java index edf69ee..3ea9b4a 100644 --- a/core/java/android/pim/EventRecurrence.java +++ b/core/java/android/pim/EventRecurrence.java @@ -408,13 +408,13 @@ public class EventRecurrence private String dayToString(Resources r, int day) { switch (day) { - case SU: return r.getString(com.android.internal.R.string.sunday); - case MO: return r.getString(com.android.internal.R.string.monday); - case TU: return r.getString(com.android.internal.R.string.tuesday); - case WE: return r.getString(com.android.internal.R.string.wednesday); - case TH: return r.getString(com.android.internal.R.string.thursday); - case FR: return r.getString(com.android.internal.R.string.friday); - case SA: return r.getString(com.android.internal.R.string.saturday); + case SU: return r.getString(com.android.internal.R.string.day_of_week_long_sunday); + case MO: return r.getString(com.android.internal.R.string.day_of_week_long_monday); + case TU: return r.getString(com.android.internal.R.string.day_of_week_long_tuesday); + case WE: return r.getString(com.android.internal.R.string.day_of_week_long_wednesday); + case TH: return r.getString(com.android.internal.R.string.day_of_week_long_thursday); + case FR: return r.getString(com.android.internal.R.string.day_of_week_long_friday); + case SA: return r.getString(com.android.internal.R.string.day_of_week_long_saturday); default: throw new IllegalArgumentException("bad day argument: " + day); } } diff --git a/core/java/android/preference/CheckBoxPreference.java b/core/java/android/preference/CheckBoxPreference.java index 1e9b7ae..cf5664c 100644 --- a/core/java/android/preference/CheckBoxPreference.java +++ b/core/java/android/preference/CheckBoxPreference.java @@ -16,6 +16,7 @@ package android.preference; +import android.app.Service; import android.content.Context; import android.content.SharedPreferences; import android.content.res.TypedArray; @@ -23,6 +24,8 @@ 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.Checkable; import android.widget.TextView; @@ -42,6 +45,9 @@ public class CheckBoxPreference extends Preference { private CharSequence mSummaryOff; private boolean mChecked; + private boolean mSendAccessibilityEventViewClickedType; + + private AccessibilityManager mAccessibilityManager; private boolean mDisableDependentsState; @@ -55,6 +61,9 @@ public class CheckBoxPreference extends Preference { mDisableDependentsState = 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) { @@ -64,14 +73,26 @@ public class CheckBoxPreference extends Preference { public CheckBoxPreference(Context context) { this(context, null); } - + @Override protected void onBindView(View view) { super.onBindView(view); - + View checkboxView = view.findViewById(com.android.internal.R.id.checkbox); 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; + + int eventType = AccessibilityEvent.TYPE_VIEW_CLICKED; + checkboxView.sendAccessibilityEventUnchecked(AccessibilityEvent.obtain(eventType)); + } } // Sync the summary view @@ -85,7 +106,7 @@ public class CheckBoxPreference extends Preference { summaryView.setText(mSummaryOff); useDefaultSummary = false; } - + if (useDefaultSummary) { final CharSequence summary = getSummary(); if (summary != null) { @@ -111,6 +132,10 @@ public class CheckBoxPreference extends Preference { boolean newValue = !isChecked(); + // in onBindView() an AccessibilityEventViewClickedType is sent to announce the change + // not sending + mSendAccessibilityEventViewClickedType = true; + if (!callChangeListener(newValue)) { return; } @@ -124,10 +149,11 @@ public class CheckBoxPreference extends Preference { * @param checked The checked state. */ public void setChecked(boolean checked) { + mChecked = checked; persistBoolean(checked); - + notifyDependencyChange(shouldDisableDependents()); notifyChanged(); diff --git a/core/java/android/preference/PreferenceScreen.java b/core/java/android/preference/PreferenceScreen.java index 5353b53..95e5432 100644 --- a/core/java/android/preference/PreferenceScreen.java +++ b/core/java/android/preference/PreferenceScreen.java @@ -22,6 +22,7 @@ import android.content.DialogInterface; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.Adapter; @@ -147,13 +148,20 @@ public final class PreferenceScreen extends PreferenceGroup implements AdapterVi ListView listView = new ListView(context); bind(listView); - Dialog dialog = mDialog = new Dialog(context, com.android.internal.R.style.Theme_NoTitleBar); + // Set the title bar if title is available, else no title bar + final CharSequence title = getTitle(); + Dialog dialog = mDialog = new Dialog(context, TextUtils.isEmpty(title) + ? com.android.internal.R.style.Theme_NoTitleBar + : com.android.internal.R.style.Theme); dialog.setContentView(listView); + if (!TextUtils.isEmpty(title)) { + dialog.setTitle(title); + } dialog.setOnDismissListener(this); if (state != null) { dialog.onRestoreInstanceState(state); } - + // Add the screen to the list of preferences screens opened as dialogs getPreferenceManager().addPreferencesScreen(dialog); diff --git a/core/java/android/provider/Browser.java b/core/java/android/provider/Browser.java index c597b3c..1ba5e25 100644 --- a/core/java/android/provider/Browser.java +++ b/core/java/android/provider/Browser.java @@ -34,6 +34,12 @@ public class Browser { Uri.parse("content://browser/bookmarks"); /** + * The inline scheme to show embedded content in a browser. + * @hide + */ + public static final Uri INLINE_URI = Uri.parse("inline:"); + + /** * The name of extra data when starting Browser with ACTION_VIEW or * ACTION_SEARCH intent. * <p> @@ -53,7 +59,48 @@ public class Browser { * identifier. */ public static final String EXTRA_APPLICATION_ID = - "com.android.browser.application_id"; + "com.android.browser.application_id"; + + /** + * The content to be rendered when url's scheme is inline. + * @hide + */ + public static final String EXTRA_INLINE_CONTENT ="com.android.browser.inline.content"; + + /** + * The encoding of the inlined content for inline scheme. + * @hide + */ + public static final String EXTRA_INLINE_ENCODING ="com.android.browser.inline.encoding"; + + /** + * The url used when the inline content is falied to render. + * @hide + */ + public static final String EXTRA_INLINE_FAILURL ="com.android.browser.inline.failurl"; + + /** + * The name of the extra data in the VIEW intent. The data is in boolean. + * <p> + * If the Browser is handling the intent and the setting for + * USE_LOCATION_FOR_SERVICES is allow, the Browser will send the location in + * the POST data if this extra data is presented and it is true. + * <p> + * pending api approval + * @hide + */ + public static final String EXTRA_APPEND_LOCATION = "com.android.browser.append_location"; + + /** + * The name of the extra data in the VIEW intent. The data is in the format of + * a byte array. + * <p> + * Any value sent here will be passed in the http request to the provided url as post data. + * <p> + * pending api approval + * @hide + */ + public static final String EXTRA_POST_DATA = "com.android.browser.post_data"; /* if you change column order you must also change indices below */ @@ -132,6 +179,7 @@ public class Browser { /** * Return a cursor pointing to a list of all the bookmarks. + * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. */ public static final Cursor getAllBookmarks(ContentResolver cr) throws @@ -143,6 +191,7 @@ public class Browser { /** * Return a cursor pointing to a list of all visited site urls. + * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. */ public static final Cursor getAllVisitedUrls(ContentResolver cr) throws @@ -154,6 +203,8 @@ public class Browser { /** * Update the visited history to acknowledge that a site has been * visited. + * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} + * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. * @param url The site being visited. * @param real Whether this is an actual visit, and should be added to the @@ -203,6 +254,8 @@ public class Browser { * of them. This is used to keep our history table to a * reasonable size. Note: it does not prune bookmarks. If the * user wants 1000 bookmarks, the user gets 1000 bookmarks. + * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} + * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} * * @param cr The ContentResolver used to access the database. */ @@ -236,6 +289,7 @@ public class Browser { /** * Returns whether there is any history to clear. + * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. * @return boolean True if the history can be cleared. */ @@ -261,6 +315,7 @@ public class Browser { /** * Delete all entries from the bookmarks/history table which are * not bookmarks. Also set all visited bookmarks to unvisited. + * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. */ public static final void clearHistory(ContentResolver cr) { @@ -270,6 +325,8 @@ public class Browser { /** * Helper function to delete all history items and revert all * bookmarks to zero visits which meet the criteria provided. + * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} + * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. * @param whereClause String to limit the items affected. * null means all items. @@ -332,6 +389,7 @@ public class Browser { /** * Delete all history items from begin to end. + * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. * @param begin First date to remove. If -1, all dates before end. * Inclusive. @@ -359,6 +417,7 @@ public class Browser { /** * Remove a specific url from the history database. + * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. * @param url url to remove. */ @@ -372,6 +431,8 @@ public class Browser { /** * Add a search string to the searches database. + * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} + * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. * @param search The string to add to the searches database. */ @@ -401,6 +462,7 @@ public class Browser { } /** * Remove all searches from the search database. + * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. */ public static final void clearSearches(ContentResolver cr) { @@ -415,6 +477,7 @@ public class Browser { /** * Request all icons from the database. + * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. * @param where Clause to be used to limit the query from the database. * Must be an allowable string to be passed into a database query. diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java index abd6934..7d03801 100644 --- a/core/java/android/provider/CallLog.java +++ b/core/java/android/provider/CallLog.java @@ -151,6 +151,9 @@ public class CallLog { int presentation, int callType, long start, int duration) { final ContentResolver resolver = context.getContentResolver(); + // TODO(Moto): Which is correct: original code, this only changes the + // number if the number is empty and never changes the caller info name. + if (false) { if (TextUtils.isEmpty(number)) { if (presentation == Connection.PRESENTATION_RESTRICTED) { number = CallerInfo.PRIVATE_NUMBER; @@ -160,7 +163,22 @@ public class CallLog { number = CallerInfo.UNKNOWN_NUMBER; } } - + } else { + // NEWCODE: From Motorola + + //If this is a private number then set the number to Private, otherwise check + //if the number field is empty and set the number to Unavailable + if (presentation == Connection.PRESENTATION_RESTRICTED) { + number = CallerInfo.PRIVATE_NUMBER; + ci.name = ""; + } else if (presentation == Connection.PRESENTATION_PAYPHONE) { + number = CallerInfo.PAYPHONE_NUMBER; + ci.name = ""; + } else if (TextUtils.isEmpty(number) || presentation == Connection.PRESENTATION_UNKNOWN) { + number = CallerInfo.UNKNOWN_NUMBER; + ci.name = ""; + } + } ContentValues values = new ContentValues(5); values.put(NUMBER, number); diff --git a/core/java/android/provider/Checkin.java b/core/java/android/provider/Checkin.java index 3c23db0..f2c275e 100644 --- a/core/java/android/provider/Checkin.java +++ b/core/java/android/provider/Checkin.java @@ -137,6 +137,8 @@ public final class Checkin { CRASHES_TRUNCATED, ELAPSED_REALTIME_SEC, ELAPSED_UPTIME_SEC, + HTTP_REQUEST, + HTTP_REUSED, HTTP_STATUS, PHONE_GSM_REGISTERED, PHONE_GPRS_ATTEMPTED, @@ -351,6 +353,3 @@ public final class Checkin { } } } - - - diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java index 3141f1a..84fe184 100644 --- a/core/java/android/provider/Contacts.java +++ b/core/java/android/provider/Contacts.java @@ -340,27 +340,33 @@ public class Contacts { } /** - * Adds a person to the My Contacts group. - * - * @param resolver the resolver to use - * @param personId the person to add to the group - * @return the URI of the group membership row - * @throws IllegalStateException if the My Contacts group can't be found + * @hide Used in vCard parser code. */ - public static Uri addToMyContactsGroup(ContentResolver resolver, long personId) { - long groupId = 0; + public static long tryGetMyContactsGroupId(ContentResolver resolver) { Cursor groupsCursor = resolver.query(Groups.CONTENT_URI, GROUPS_PROJECTION, Groups.SYSTEM_ID + "='" + Groups.GROUP_MY_CONTACTS + "'", null, null); if (groupsCursor != null) { try { if (groupsCursor.moveToFirst()) { - groupId = groupsCursor.getLong(0); + return groupsCursor.getLong(0); } } finally { groupsCursor.close(); } } + return 0; + } + /** + * Adds a person to the My Contacts group. + * + * @param resolver the resolver to use + * @param personId the person to add to the group + * @return the URI of the group membership row + * @throws IllegalStateException if the My Contacts group can't be found + */ + public static Uri addToMyContactsGroup(ContentResolver resolver, long personId) { + long groupId = tryGetMyContactsGroupId(resolver); if (groupId == 0) { throw new IllegalStateException("Failed to find the My Contacts group"); } @@ -869,6 +875,17 @@ public class Contacts { public static final int TYPE_OTHER = 3; /** + * @hide This is temporal. TYPE_MOBILE should be added to TYPE in the future. + */ + public static final int MOBILE_EMAIL_TYPE_INDEX = 2; + + /** + * @hide This is temporal. TYPE_MOBILE should be added to TYPE in the future. + * This is not "mobile" but "CELL" since vCard uses it for identifying mobile phone. + */ + public static final String MOBILE_EMAIL_TYPE_NAME = "_AUTO_CELL"; + + /** * The user defined label for the the contact method. * <P>Type: TEXT</P> */ @@ -1005,7 +1022,13 @@ public class Contacts { } } else { if (!TextUtils.isEmpty(label)) { - display = label; + if (label.toString().equals(MOBILE_EMAIL_TYPE_NAME)) { + display = + context.getString( + com.android.internal.R.string.mobileEmailTypeName); + } else { + display = label; + } } } break; diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index b6f96c4..21e5865 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -344,7 +344,10 @@ public final class MediaStore // Check if file exists with a FileInputStream FileInputStream stream = new FileInputStream(imagePath); try { - return insertImage(cr, BitmapFactory.decodeFile(imagePath), name, description); + Bitmap bm = BitmapFactory.decodeFile(imagePath); + String ret = insertImage(cr, bm, name, description); + bm.recycle(); + return ret; } finally { try { stream.close(); @@ -719,9 +722,15 @@ public final class MediaStore */ public static String keyFor(String name) { if (name != null) { + boolean sortfirst = false; if (name.equals(android.media.MediaFile.UNKNOWN_STRING)) { return "\001"; } + // Check if the first character is \001. We use this to + // force sorting of certain special files, like the silent ringtone. + if (name.startsWith("\001")) { + sortfirst = true; + } name = name.trim().toLowerCase(); if (name.startsWith("the ")) { name = name.substring(4); @@ -737,7 +746,7 @@ public final class MediaStore name.endsWith(", a") || name.endsWith(",a")) { name = name.substring(0, name.lastIndexOf(',')); } - name = name.replaceAll("[\\[\\]\\(\\)'.,?!]", "").trim(); + name = name.replaceAll("[\\[\\]\\(\\)\"'.,?!]", "").trim(); if (name.length() > 0) { // Insert a separator between the characters to avoid // matches on a partial character. If we ever change @@ -750,7 +759,11 @@ public final class MediaStore b.append('.'); } name = b.toString(); - return DatabaseUtils.getCollationKey(name); + String key = DatabaseUtils.getCollationKey(name); + if (sortfirst) { + key = "\001" + key; + } + return key; } else { return ""; } @@ -797,7 +810,7 @@ public final class MediaStore /** * The default sort order for this table */ - public static final String DEFAULT_SORT_ORDER = TITLE; + public static final String DEFAULT_SORT_ORDER = TITLE_KEY; /** * Activity Action: Start SoundRecorder application. @@ -894,7 +907,7 @@ public final class MediaStore /** * The default sort order for this table */ - public static final String DEFAULT_SORT_ORDER = TITLE; + public static final String DEFAULT_SORT_ORDER = TITLE_KEY; /** * The ID of the audio file diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 4dd6524..aa583ac 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -148,7 +148,7 @@ public final class Settings { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_WIFI_SETTINGS = "android.settings.WIFI_SETTINGS"; - + /** * Activity Action: Show settings to allow configuration of a static IP * address for Wi-Fi. @@ -305,7 +305,7 @@ public final class Settings { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_QUICK_LAUNCH_SETTINGS = "android.settings.QUICK_LAUNCH_SETTINGS"; - + /** * Activity Action: Show settings to manage installed applications. * <p> @@ -319,7 +319,7 @@ public final class Settings { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_MANAGE_APPLICATIONS_SETTINGS = "android.settings.MANAGE_APPLICATIONS_SETTINGS"; - + /** * Activity Action: Show settings for system update functionality. * <p> @@ -329,7 +329,7 @@ public final class Settings { * Input: Nothing. * <p> * Output: Nothing. - * + * * @hide */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) @@ -349,7 +349,7 @@ public final class Settings { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_SYNC_SETTINGS = "android.settings.SYNC_SETTINGS"; - + /** * Activity Action: Show settings for selecting the network operator. * <p> @@ -404,7 +404,7 @@ public final class Settings { @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_MEMORY_CARD_SETTINGS = "android.settings.MEMORY_CARD_SETTINGS"; - + // End of Intent actions for Settings private static final String JID_RESOURCE_PREFIX = "android"; @@ -495,7 +495,7 @@ public final class Settings { public static final String SYS_PROP_SETTING_VERSION = "sys.settings_system_version"; private static volatile NameValueCache mNameValueCache = null; - + private static final HashSet<String> MOVED_TO_SECURE; static { MOVED_TO_SECURE = new HashSet<String>(30); @@ -901,12 +901,12 @@ public final class Settings { * plugged in. */ public static final int WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED = 1; - + /** * Value for {@link #WIFI_SLEEP_POLICY} to never go to sleep. */ public static final int WIFI_SLEEP_POLICY_NEVER = 2; - + /** * Whether to use static IP and other static network attributes. * <p> @@ -1025,6 +1025,14 @@ public final class Settings { public static final String SCREEN_OFF_TIMEOUT = "screen_off_timeout"; /** + * If 0, the compatibility mode is off for all applications. + * If 1, older applications run under compatibility mode. + * TODO: remove this settings before code freeze (bug/1907571) + * @hide + */ + public static final String COMPATIBILITY_MODE = "compatibility_mode"; + + /** * The screen backlight brightness between 0 and 255. */ public static final String SCREEN_BRIGHTNESS = "screen_brightness"; @@ -1115,12 +1123,12 @@ public final class Settings { * Note: This is a one-off setting that will be removed in the future * when there is profile support. For this reason, it is kept hidden * from the public APIs. - * + * * @hide */ - public static final String NOTIFICATIONS_USE_RING_VOLUME = + public static final String NOTIFICATIONS_USE_RING_VOLUME = "notifications_use_ring_volume"; - + /** * The mapping of stream type (integer) to its setting. */ @@ -1188,7 +1196,7 @@ public final class Settings { * feature converts two spaces to a "." and space. */ public static final String TEXT_AUTO_PUNCTUATE = "auto_punctuate"; - + /** * Setting to showing password characters in text editors. 1 = On, 0 = Off */ @@ -1266,17 +1274,125 @@ public final class Settings { public static final String DTMF_TONE_WHEN_DIALING = "dtmf_tone"; /** + * CDMA only settings + * DTMF tone type played by the dialer when dialing. + * 0 = Normal + * 1 = Long + * @hide + */ + public static final String DTMF_TONE_TYPE_WHEN_DIALING = "dtmf_tone_type"; + + /** + * CDMA only settings + * Emergency Tone 0 = Off + * 1 = Alert + * 2 = Vibrate + * @hide + */ + public static final String EMERGENCY_TONE = "emergency_tone"; + + /** + * CDMA only settings + * Whether the auto retry is enabled. The value is + * boolean (1 or 0). + * @hide + */ + public static final String CALL_AUTO_RETRY = "call_auto_retry"; + + /** + * Whether the hearing aid is enabled. The value is + * boolean (1 or 0). + * @hide + */ + public static final String HEARING_AID = "hearing_aid"; + + /** + * CDMA only settings + * TTY Mode + * 0 = OFF + * 1 = FULL + * 2 = VCO + * 3 = HCO + * @hide + */ + public static final String TTY_MODE = "tty_mode"; + + /** * Whether the sounds effects (key clicks, lid open ...) are enabled. The value is * boolean (1 or 0). */ public static final String SOUND_EFFECTS_ENABLED = "sound_effects_enabled"; - + /** * Whether the haptic feedback (long presses, ...) are enabled. The value is * boolean (1 or 0). */ public static final String HAPTIC_FEEDBACK_ENABLED = "haptic_feedback_enabled"; + /** + * Whether live web suggestions while the user types into search dialogs are + * enabled. Browsers and other search UIs should respect this, as it allows + * a user to avoid sending partial queries to a search engine, if it poses + * any privacy concern. The value is boolean (1 or 0). + */ + public static final String SHOW_WEB_SUGGESTIONS = "show_web_suggestions"; + + /** + * Settings to backup. This is here so that it's in the same place as the settings + * keys and easy to update. + * @hide + */ + public static final String[] SETTINGS_TO_BACKUP = { + STAY_ON_WHILE_PLUGGED_IN, + END_BUTTON_BEHAVIOR, + WIFI_SLEEP_POLICY, + WIFI_USE_STATIC_IP, + WIFI_STATIC_IP, + WIFI_STATIC_GATEWAY, + WIFI_STATIC_NETMASK, + WIFI_STATIC_DNS1, + WIFI_STATIC_DNS2, + BLUETOOTH_DISCOVERABILITY, + BLUETOOTH_DISCOVERABILITY_TIMEOUT, + DIM_SCREEN, + SCREEN_OFF_TIMEOUT, + SCREEN_BRIGHTNESS, + VIBRATE_ON, + NOTIFICATIONS_USE_RING_VOLUME, + MODE_RINGER, + MODE_RINGER_STREAMS_AFFECTED, + MUTE_STREAMS_AFFECTED, + VOLUME_VOICE, + VOLUME_SYSTEM, + VOLUME_RING, + VOLUME_MUSIC, + VOLUME_ALARM, + VOLUME_NOTIFICATION, + VOLUME_VOICE + APPEND_FOR_LAST_AUDIBLE, + VOLUME_SYSTEM + APPEND_FOR_LAST_AUDIBLE, + VOLUME_RING + APPEND_FOR_LAST_AUDIBLE, + VOLUME_MUSIC + APPEND_FOR_LAST_AUDIBLE, + VOLUME_ALARM + APPEND_FOR_LAST_AUDIBLE, + VOLUME_NOTIFICATION + APPEND_FOR_LAST_AUDIBLE, + TEXT_AUTO_REPLACE, + TEXT_AUTO_CAPS, + TEXT_AUTO_PUNCTUATE, + TEXT_SHOW_PASSWORD, + AUTO_TIME, + TIME_12_24, + DATE_FORMAT, + ACCELEROMETER_ROTATION, + DTMF_TONE_WHEN_DIALING, + DTMF_TONE_TYPE_WHEN_DIALING, + EMERGENCY_TONE, + CALL_AUTO_RETRY, + HEARING_AID, + TTY_MODE, + SOUND_EFFECTS_ENABLED, + HAPTIC_FEEDBACK_ENABLED, + SHOW_WEB_SUGGESTIONS + }; + // Settings moved to Settings.Secure /** @@ -1321,7 +1437,7 @@ public final class Settings { */ @Deprecated public static final String INSTALL_NON_MARKET_APPS = Secure.INSTALL_NON_MARKET_APPS; - + /** * @deprecated Use {@link android.provider.Settings.Secure#LOCATION_PROVIDERS_ALLOWED} * instead @@ -1334,7 +1450,7 @@ public final class Settings { */ @Deprecated public static final String LOGGING_ID = Secure.LOGGING_ID; - + /** * @deprecated Use {@link android.provider.Settings.Secure#NETWORK_PREFERENCE} instead */ @@ -1374,7 +1490,7 @@ public final class Settings { */ @Deprecated public static final String USB_MASS_STORAGE_ENABLED = Secure.USB_MASS_STORAGE_ENABLED; - + /** * @deprecated Use {@link android.provider.Settings.Secure#USE_GOOGLE_MAIL} instead */ @@ -1412,7 +1528,7 @@ public final class Settings { @Deprecated public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY = Secure.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY; - + /** * @deprecated Use {@link android.provider.Settings.Secure#WIFI_NUM_OPEN_NETWORKS_KEPT} * instead @@ -1448,7 +1564,7 @@ public final class Settings { @Deprecated public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS = Secure.WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS; - + /** * @deprecated Use * {@link android.provider.Settings.Secure#WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED} instead @@ -1824,19 +1940,19 @@ public final class Settings { * Whether the device has been provisioned (0 = false, 1 = true) */ public static final String DEVICE_PROVISIONED = "device_provisioned"; - + /** * List of input methods that are currently enabled. This is a string * containing the IDs of all enabled input methods, each ID separated * by ':'. */ public static final String ENABLED_INPUT_METHODS = "enabled_input_methods"; - + /** * Host name and port for a user-selected proxy. */ public static final String HTTP_PROXY = "http_proxy"; - + /** * Whether the package installer should allow installation of apps downloaded from * sources other than the Android Market (vending machine). @@ -1845,12 +1961,12 @@ public final class Settings { * 0 = only allow installing from the Android Market */ public static final String INSTALL_NON_MARKET_APPS = "install_non_market_apps"; - + /** * Comma-separated list of location providers that activities may access. */ public static final String LOCATION_PROVIDERS_ALLOWED = "location_providers_allowed"; - + /** * The Logging ID (a unique 64-bit value) as a hex string. * Used as a pseudonymous identifier for logging. @@ -1872,19 +1988,19 @@ public final class Settings { * connectivity service should touch this. */ public static final String NETWORK_PREFERENCE = "network_preference"; - - /** + + /** */ public static final String PARENTAL_CONTROL_ENABLED = "parental_control_enabled"; - - /** + + /** */ public static final String PARENTAL_CONTROL_LAST_UPDATE = "parental_control_last_update"; - - /** + + /** */ public static final String PARENTAL_CONTROL_REDIRECT_URL = "parental_control_redirect_url"; - + /** * Settings classname to launch when Settings is clicked from All * Applications. Needed because of user testing between the old @@ -1892,18 +2008,67 @@ public final class Settings { */ // TODO: 881807 public static final String SETTINGS_CLASSNAME = "settings_classname"; - + /** * USB Mass Storage Enabled */ public static final String USB_MASS_STORAGE_ENABLED = "usb_mass_storage_enabled"; - + /** * If this setting is set (to anything), then all references * to Gmail on the device must change to Google Mail. */ public static final String USE_GOOGLE_MAIL = "use_google_mail"; - + + /** + * If accessibility is enabled. + */ + public static final String ACCESSIBILITY_ENABLED = "accessibility_enabled"; + + /** + * List of the enabled accessibility providers. + */ + public static final String ENABLED_ACCESSIBILITY_SERVICES = + "enabled_accessibility_services"; + + /** + * Setting to always use the default text-to-speech settings regardless + * of the application settings. + * 1 = override application settings, + * 0 = use application settings (if specified). + */ + public static final String TTS_USE_DEFAULTS = "tts_use_defaults"; + + /** + * Default text-to-speech engine speech rate. 100 = 1x + */ + public static final String TTS_DEFAULT_RATE = "tts_default_rate"; + + /** + * Default text-to-speech engine pitch. 100 = 1x + */ + public static final String TTS_DEFAULT_PITCH = "tts_default_pitch"; + + /** + * Default text-to-speech engine. + */ + public static final String TTS_DEFAULT_SYNTH = "tts_default_synth"; + + /** + * Default text-to-speech language. + */ + public static final String TTS_DEFAULT_LANG = "tts_default_lang"; + + /** + * Default text-to-speech country. + */ + public static final String TTS_DEFAULT_COUNTRY = "tts_default_country"; + + /** + * Default text-to-speech locale variant. + */ + public static final String TTS_DEFAULT_VARIANT = "tts_default_variant"; + /** * Whether to notify the user of open networks. * <p> @@ -1915,64 +2080,64 @@ public final class Settings { */ public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON = "wifi_networks_available_notification_on"; - + /** * Delay (in seconds) before repeating the Wi-Fi networks available notification. * Connecting to a network will reset the timer. */ public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY = "wifi_networks_available_repeat_delay"; - + /** * The number of radio channels that are allowed in the local * 802.11 regulatory domain. * @hide */ public static final String WIFI_NUM_ALLOWED_CHANNELS = "wifi_num_allowed_channels"; - + /** * When the number of open networks exceeds this number, the * least-recently-used excess networks will be removed. */ public static final String WIFI_NUM_OPEN_NETWORKS_KEPT = "wifi_num_open_networks_kept"; - + /** * Whether the Wi-Fi should be on. Only the Wi-Fi service should touch this. */ public static final String WIFI_ON = "wifi_on"; - + /** * The acceptable packet loss percentage (range 0 - 100) before trying * another AP on the same network. */ public static final String WIFI_WATCHDOG_ACCEPTABLE_PACKET_LOSS_PERCENTAGE = "wifi_watchdog_acceptable_packet_loss_percentage"; - + /** * The number of access points required for a network in order for the * watchdog to monitor it. */ public static final String WIFI_WATCHDOG_AP_COUNT = "wifi_watchdog_ap_count"; - + /** * The delay between background checks. */ public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_DELAY_MS = "wifi_watchdog_background_check_delay_ms"; - + /** * Whether the Wi-Fi watchdog is enabled for background checking even * after it thinks the user has connected to a good access point. */ public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_ENABLED = "wifi_watchdog_background_check_enabled"; - + /** * The timeout for a background ping */ public static final String WIFI_WATCHDOG_BACKGROUND_CHECK_TIMEOUT_MS = "wifi_watchdog_background_check_timeout_ms"; - + /** * The number of initial pings to perform that *may* be ignored if they * fail. Again, if these fail, they will *not* be used in packet loss @@ -1981,7 +2146,7 @@ public final class Settings { */ public static final String WIFI_WATCHDOG_INITIAL_IGNORED_PING_COUNT = "wifi_watchdog_initial_ignored_ping_count"; - + /** * The maximum number of access points (per network) to attempt to test. * If this number is reached, the watchdog will no longer monitor the @@ -1989,7 +2154,7 @@ public final class Settings { * networks containing multiple APs whose DNS does not respond to pings. */ public static final String WIFI_WATCHDOG_MAX_AP_CHECKS = "wifi_watchdog_max_ap_checks"; - + /** * Whether the Wi-Fi watchdog is enabled. */ @@ -2004,24 +2169,24 @@ public final class Settings { * The number of pings to test if an access point is a good connection. */ public static final String WIFI_WATCHDOG_PING_COUNT = "wifi_watchdog_ping_count"; - + /** * The delay between pings. */ public static final String WIFI_WATCHDOG_PING_DELAY_MS = "wifi_watchdog_ping_delay_ms"; - + /** * The timeout per ping. */ public static final String WIFI_WATCHDOG_PING_TIMEOUT_MS = "wifi_watchdog_ping_timeout_ms"; - + /** * 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. */ public static final String WIFI_MAX_DHCP_RETRY_COUNT = "wifi_max_dhcp_retry_count"; - + /** * Maximum amount of time in milliseconds to hold a wakelock while waiting for mobile * data connectivity to be established after a disconnect from Wi-Fi. @@ -2051,20 +2216,29 @@ public final class Settings { public static final String CDMA_SUBSCRIPTION_MODE = "subscription_mode"; /** - * represents current active phone class - * 1 = GSM-Phone, 0 = CDMA-Phone + * The preferred network mode 7 = Global + * 6 = EvDo only + * 5 = CDMA w/o EvDo + * 4 = CDMA / EvDo auto + * 3 = GSM / WCDMA auto + * 2 = WCDMA only + * 1 = GSM only + * 0 = GSM / WCDMA preferred * @hide */ - public static final String CURRENT_ACTIVE_PHONE = "current_active_phone"; + public static final String PREFERRED_NETWORK_MODE = + "preferred_network_mode"; /** - * The preferred network mode 7 = Global, CDMA default - * 4 = CDMA only - * 3 = GSM/UMTS only + * The preferred TTY mode 0 = TTy Off, CDMA default + * 1 = TTY Full + * 2 = TTY HCO + * 3 = TTY VCO * @hide */ - public static final String PREFERRED_NETWORK_MODE = - "preferred_network_mode"; + public static final String PREFERRED_TTY_MODE = + "preferred_tty_mode"; + /** * CDMA Cell Broadcast SMS @@ -2100,6 +2274,71 @@ public final class Settings { public static final String TTY_MODE_ENABLED = "tty_mode_enabled"; /** + * Flag for allowing service provider to use location information to improve products and + * services. + * Type: int ( 0 = disallow, 1 = allow ) + * @hide + */ + public static final String USE_LOCATION_FOR_SERVICES = "use_location"; + + /** + * Controls whether settings backup is enabled. + * Type: int ( 0 = disabled, 1 = enabled ) + * @hide + */ + public static final String BACKUP_ENABLED = "backup_enabled"; + + /** + * Indicates whether settings backup has been fully provisioned. + * Type: int ( 0 = unprovisioned, 1 = fully provisioned ) + * @hide + */ + public static final String BACKUP_PROVISIONED = "backup_provisioned"; + + /** + * Component of the transport to use for backup/restore. + * @hide + */ + public static final String BACKUP_TRANSPORT = "backup_transport"; + + /** + * Version for which the setup wizard was last shown. Bumped for + * each release when there is new setup information to show. + * @hide + */ + public static final String LAST_SETUP_SHOWN = "last_setup_shown"; + + /** + * @hide + */ + public static final String[] SETTINGS_TO_BACKUP = { + ADB_ENABLED, + ALLOW_MOCK_LOCATION, + INSTALL_NON_MARKET_APPS, + PARENTAL_CONTROL_ENABLED, + PARENTAL_CONTROL_REDIRECT_URL, + USB_MASS_STORAGE_ENABLED, + ACCESSIBILITY_ENABLED, + ENABLED_ACCESSIBILITY_SERVICES, + TTS_USE_DEFAULTS, + TTS_DEFAULT_RATE, + TTS_DEFAULT_PITCH, + TTS_DEFAULT_SYNTH, + TTS_DEFAULT_LANG, + TTS_DEFAULT_COUNTRY, + WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, + WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, + WIFI_NUM_ALLOWED_CHANNELS, + WIFI_NUM_OPEN_NETWORKS_KEPT, + BACKGROUND_DATA, + PREFERRED_NETWORK_MODE, + PREFERRED_TTY_MODE, + CDMA_CELL_BROADCAST_SMS, + PREFERRED_CDMA_SUBSCRIPTION, + ENHANCED_VOICE_PRIVACY_ENABLED + }; + + /** * Helper method for determining if a location provider is enabled. * @param cr the content resolver to use * @param provider the location provider to query @@ -2115,7 +2354,7 @@ public final class Settings { allowedProviders.startsWith(provider + ",") || allowedProviders.endsWith("," + provider)); } - return false; + return false; } /** @@ -2139,7 +2378,7 @@ public final class Settings { putString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, provider); } } - + /** * Gservices settings, containing the network names for Google's * various services. This table holds simple name/addr pairs. @@ -2160,6 +2399,13 @@ public final class Settings { public static final String CHANGED_ACTION = "com.google.gservices.intent.action.GSERVICES_CHANGED"; + /** + * Intent action to override Gservices for testing. (Requires WRITE_GSERVICES permission.) + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String OVERRIDE_ACTION = + "com.google.gservices.intent.action.GSERVICES_OVERRIDE"; + private static volatile NameValueCache mNameValueCache = null; private static final Object mNameValueCacheLock = new Object(); @@ -2260,7 +2506,7 @@ public final class Settings { * Event tags from the kernel event log to upload during checkin. */ public static final String CHECKIN_EVENTS = "checkin_events"; - + /** * Event tags for list of services to upload during checkin. */ @@ -2428,12 +2674,34 @@ public final class Settings { public static final String GMAIL_BUFFER_SERVER_RESPONSE = "gmail_buffer_server_response"; /** + * The maximum size in bytes allowed for the provider to gzip a protocol buffer uploaded to + * the server. + */ + public static final String GMAIL_MAX_GZIP_SIZE = "gmail_max_gzip_size_bytes"; + + /** * Controls whether Gmail will discard uphill operations that repeatedly fail. Value must be * an integer where non-zero means true. Defaults to 1. */ public static final String GMAIL_DISCARD_ERROR_UPHILL_OP = "gmail_discard_error_uphill_op"; /** + * Controls how many attempts Gmail will try to upload an uphill operations before it + * abandons the operation. Defaults to 20. + */ + public static final String GMAIL_NUM_RETRY_UPHILL_OP = "gmail_discard_error_uphill_op"; + + /** + * the transcoder URL for mobile devices. + */ + public static final String TRANSCODER_URL = "mobile_transcoder_url"; + + /** + * URL that points to the privacy terms of the Google Talk service. + */ + public static final String GTALK_TERMS_OF_SERVICE_URL = "gtalk_terms_of_service_url"; + + /** * Hostname of the GTalk server. */ public static final String GTALK_SERVICE_HOSTNAME = "gtalk_hostname"; @@ -2561,6 +2829,21 @@ public final class Settings { "gtalk_ssl_handshake_timeout_ms"; /** + * Compress the gtalk stream. + */ + public static final String GTALK_COMPRESS = "gtalk_compress"; + + /** + * This is the timeout for which Google Talk will send the message using bareJID. In a + * established chat between two XMPP endpoints, Google Talk uses fullJID in the format + * of user@domain/resource in order to send the message to the specific client. However, + * if Google Talk hasn't received a message from that client after some time, it would + * fall back to use the bareJID, which would broadcast the message to all clients for + * the other user. + */ + public static final String GTALK_USE_BARE_JID_TIMEOUT_MS = "gtalk_use_barejid_timeout_ms"; + + /** * Enable use of ssl session caching. * 'db' - save each session in a (per process) database * 'file' - save each session in a (per process) file @@ -2657,6 +2940,20 @@ public final class Settings { public static final String VENDING_TAB_2_TITLE = "vending_tab_2_title"; /** + * Frequency in milliseconds at which we should request MCS heartbeats + * from the Vending Machine client. + */ + public static final String VENDING_HEARTBEAT_FREQUENCY_MS = + "vending_heartbeat_frequency_ms"; + + /** + * Frequency in milliseconds at which we should resend pending download + * requests to the API Server from the Vending Machine client. + */ + public static final String VENDING_PENDING_DOWNLOAD_RESEND_FREQUENCY_MS = + "vending_pd_resend_frequency_ms"; + + /** * URL that points to the legal terms of service to display in Settings. * <p> * This should be a https URL. For a pretty user-friendly URL, use @@ -2796,12 +3093,12 @@ public final class Settings { * out without asking for use permit, to limit the un-authorized SMS * usage. */ - public static final String SMS_OUTGOING_CEHCK_INTERVAL_MS = + public static final String SMS_OUTGOING_CHECK_INTERVAL_MS = "sms_outgoing_check_interval_ms"; /** * The number of outgoing SMS sent without asking for user permit - * (of {@link #SMS_OUTGOING_CEHCK_INTERVAL_MS} + * (of {@link #SMS_OUTGOING_CHECK_INTERVAL_MS} */ public static final String SMS_OUTGOING_CEHCK_MAX_COUNT = "sms_outgoing_check_max_count"; @@ -2950,13 +3247,21 @@ public final class Settings { public static final String BATTERY_DISCHARGE_DURATION_THRESHOLD = "battery_discharge_duration_threshold"; public static final String BATTERY_DISCHARGE_THRESHOLD = "battery_discharge_threshold"; - + /** * An email address that anr bugreports should be sent to. */ public static final String ANR_BUGREPORT_RECIPIENT = "anr_bugreport_recipient"; /** + * Flag for allowing service provider to use location information to improve products and + * services. + * Type: int ( 0 = disallow, 1 = allow ) + * @deprecated + */ + public static final String USE_LOCATION_FOR_SERVICES = "use_location"; + + /** * @deprecated * @hide */ @@ -3094,7 +3399,7 @@ public final class Settings { /** * Add a new bookmark to the system. - * + * * @param cr The ContentResolver to query. * @param intent The desired target of the bookmark. * @param title Bookmark title that is shown to the user; null if none @@ -3159,7 +3464,7 @@ public final class Settings { /** * Return the title as it should be displayed to the user. This takes * care of localizing bookmarks that point to activities. - * + * * @param context A context. * @param cursor A cursor pointing to the row whose title should be * returned. The cursor must contain at least the {@link #TITLE} @@ -3174,24 +3479,24 @@ public final class Settings { throw new IllegalArgumentException( "The cursor must contain the TITLE and INTENT columns."); } - + String title = cursor.getString(titleColumn); if (!TextUtils.isEmpty(title)) { return title; } - + String intentUri = cursor.getString(intentColumn); if (TextUtils.isEmpty(intentUri)) { return ""; } - + Intent intent; try { intent = Intent.getIntent(intentUri); } catch (URISyntaxException e) { return ""; } - + PackageManager packageManager = context.getPackageManager(); ResolveInfo info = packageManager.resolveActivity(intent, 0); return info != null ? info.loadLabel(packageManager) : ""; @@ -3247,4 +3552,3 @@ public final class Settings { return "android-" + Long.toHexString(androidId); } } - diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index a4145c4..4078fa6 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -466,6 +466,24 @@ public final class Telephony { */ public static final class Intents { /** + * Set by BroadcastReceiver. Indicates the message was handled + * successfully. + */ + public static final int RESULT_SMS_HANDLED = 1; + + /** + * Set by BroadcastReceiver. Indicates a generic error while + * processing the message. + */ + public static final int RESULT_SMS_GENERIC_ERROR = 2; + + /** + * Set by BroadcastReceiver. Indicates insufficient memory to store + * the message. + */ + public static final int RESULT_SMS_OUT_OF_MEMORY = 3; + + /** * Broadcast Action: A new text based SMS message has been received * by the device. The intent will have the following extra * values:</p> @@ -476,7 +494,10 @@ public final class Telephony { * </ul> * * <p>The extra values can be extracted using - * {@link #getMessagesFromIntent(Intent)}</p> + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String SMS_RECEIVED_ACTION = @@ -493,7 +514,10 @@ public final class Telephony { * </ul> * * <p>The extra values can be extracted using - * {@link #getMessagesFromIntent(Intent)}</p> + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String DATA_SMS_RECEIVED_ACTION = @@ -510,6 +534,9 @@ public final class Telephony { * <li><em>pduType (Integer)</em> - The WAP PDU type</li> * <li><em>data</em> - The data payload of the message</li> * </ul> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String WAP_PUSH_RECEIVED_ACTION = diff --git a/core/java/android/server/BluetoothDeviceService.java b/core/java/android/server/BluetoothDeviceService.java index 8e5cee9..8c843ef 100644 --- a/core/java/android/server/BluetoothDeviceService.java +++ b/core/java/android/server/BluetoothDeviceService.java @@ -372,6 +372,10 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { mEventLoop.onModeChanged(getModeNative()); } + if (mIsAirplaneSensitive && isAirplaneModeOn()) { + disable(false); + } + } } @@ -1220,6 +1224,8 @@ public class BluetoothDeviceService extends IBluetoothDevice.Stub { break; } pw.println("getHeadsetAddress() = " + headset.getHeadsetAddress()); + pw.println("getBatteryUsageHint() = " + headset.getBatteryUsageHint()); + headset.close(); } diff --git a/core/java/android/server/search/SearchManagerService.java b/core/java/android/server/search/SearchManagerService.java index 03623d6..373e61f 100644 --- a/core/java/android/server/search/SearchManagerService.java +++ b/core/java/android/server/search/SearchManagerService.java @@ -17,48 +17,69 @@ package android.server.search; import android.app.ISearchManager; +import android.app.ISearchManagerCallback; +import android.app.SearchDialog; +import android.app.SearchManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.content.res.Configuration; +import android.os.Bundle; import android.os.Handler; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; /** * This is a simplified version of the Search Manager service. It no longer handles - * presentation (UI). Its function is to maintain the map & list of "searchable" + * presentation (UI). Its function is to maintain the map & list of "searchable" * items, which provides a mapping from individual activities (where a user might have * invoked search) to specific searchable activities (where the search will be dispatched). */ public class SearchManagerService extends ISearchManager.Stub + implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { // general debugging support private static final String TAG = "SearchManagerService"; - private static final boolean DEBUG = false; - - // configuration choices - private static final boolean IMMEDIATE_SEARCHABLES_UPDATE = true; + private static final boolean DBG = false; // class maintenance and general shared data private final Context mContext; private final Handler mHandler; private boolean mSearchablesDirty; - private Searchables mSearchables; - + private final Searchables mSearchables; + + final SearchDialog mSearchDialog; + ISearchManagerCallback mCallback = null; + + private final boolean mDisabledOnBoot; + + private static final String DISABLE_SEARCH_PROPERTY = "dev.disablesearchdialog"; + /** * Initializes the Search Manager service in the provided system context. * Only one instance of this object should be created! * * @param context to use for accessing DB, window manager, etc. */ - public SearchManagerService(Context context) { + public SearchManagerService(Context context) { mContext = context; mHandler = new Handler(); mSearchablesDirty = true; mSearchables = new Searchables(context); - + mSearchDialog = new SearchDialog(context); + mSearchDialog.setOnCancelListener(this); + mSearchDialog.setOnDismissListener(this); + // Setup the infrastructure for updating and maintaining the list // of searchable activities. IntentFilter filter = new IntentFilter(); @@ -67,17 +88,18 @@ public class SearchManagerService extends ISearchManager.Stub filter.addAction(Intent.ACTION_PACKAGE_CHANGED); filter.addDataScheme("package"); mContext.registerReceiver(mIntentReceiver, filter, null, mHandler); - + // After startup settles down, preload the searchables list, // which will reduce the delay when the search UI is invoked. - if (IMMEDIATE_SEARCHABLES_UPDATE) { - mHandler.post(mRunUpdateSearchable); - } + mHandler.post(mRunUpdateSearchable); + + // allows disabling of search dialog for stress testing runs + mDisabledOnBoot = !TextUtils.isEmpty(SystemProperties.get(DISABLE_SEARCH_PROPERTY)); } - + /** * Listens for intent broadcasts. - * + * * The primary purpose here is to refresh the "searchables" list * if packages are added/removed. */ @@ -85,29 +107,25 @@ public class SearchManagerService extends ISearchManager.Stub @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - + // First, test for intents that matter at any time if (action.equals(Intent.ACTION_PACKAGE_ADDED) || action.equals(Intent.ACTION_PACKAGE_REMOVED) || action.equals(Intent.ACTION_PACKAGE_CHANGED)) { mSearchablesDirty = true; - if (IMMEDIATE_SEARCHABLES_UPDATE) { - mHandler.post(mRunUpdateSearchable); - } + mHandler.post(mRunUpdateSearchable); return; } } }; - + /** * This runnable (for the main handler / UI thread) will update the searchables list. */ private Runnable mRunUpdateSearchable = new Runnable() { public void run() { - if (mSearchablesDirty) { - updateSearchables(); - } - } + updateSearchablesIfDirty(); + } }; /** @@ -115,42 +133,251 @@ public class SearchManagerService extends ISearchManager.Stub * a package add/remove broadcast message. */ private void updateSearchables() { + if (DBG) debug("updateSearchables()"); mSearchables.buildSearchableList(); mSearchablesDirty = false; } /** + * Updates the list of searchables if needed. + */ + private void updateSearchablesIfDirty() { + if (mSearchablesDirty) { + updateSearchables(); + } + } + + /** * Returns the SearchableInfo for a given activity * * @param launchActivity The activity from which we're launching this search. * @param globalSearch If false, this will only launch the search that has been specifically - * defined by the application (which is usually defined as a local search). If no default + * defined by the application (which is usually defined as a local search). If no default * search is defined in the current application or activity, no search will be launched. * If true, this will always launch a platform-global (e.g. web-based) search instead. * @return Returns a SearchableInfo record describing the parameters of the search, * or null if no searchable metadata was available. */ public SearchableInfo getSearchableInfo(ComponentName launchActivity, boolean globalSearch) { - // final check. however we should try to avoid this, because - // it slows down the entry into the UI. - if (mSearchablesDirty) { - updateSearchables(); - } + updateSearchablesIfDirty(); SearchableInfo si = null; if (globalSearch) { si = mSearchables.getDefaultSearchable(); } else { + if (launchActivity == null) { + Log.e(TAG, "getSearchableInfo(), activity == null"); + return null; + } si = mSearchables.getSearchableInfo(launchActivity); } return si; } - + /** * Returns a list of the searchable activities that can be included in global search. */ public List<SearchableInfo> getSearchablesInGlobalSearch() { + updateSearchablesIfDirty(); return mSearchables.getSearchablesInGlobalSearchList(); } + /** + * Launches the search UI on the main thread of the service. + * + * @see SearchManager#startSearch(String, boolean, ComponentName, Bundle, boolean) + */ + public void startSearch(final String initialQuery, + final boolean selectInitialQuery, + final ComponentName launchActivity, + final Bundle appSearchData, + final boolean globalSearch, + final ISearchManagerCallback searchManagerCallback) { + if (DBG) debug("startSearch()"); + Runnable task = new Runnable() { + public void run() { + performStartSearch(initialQuery, + selectInitialQuery, + launchActivity, + appSearchData, + globalSearch, + searchManagerCallback); + } + }; + mHandler.post(task); + } + + /** + * Actually launches the search. This must be called on the service UI thread. + */ + /*package*/ void performStartSearch(String initialQuery, + boolean selectInitialQuery, + ComponentName launchActivity, + Bundle appSearchData, + boolean globalSearch, + ISearchManagerCallback searchManagerCallback) { + if (DBG) debug("performStartSearch()"); + + if (mDisabledOnBoot) { + Log.d(TAG, "ignoring start search request because " + DISABLE_SEARCH_PROPERTY + + " system property is set."); + return; + } + + mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData, + globalSearch); + if (searchManagerCallback != null) { + mCallback = searchManagerCallback; + } + } + + /** + * Cancels the search dialog. Can be called from any thread. + */ + public void stopSearch() { + if (DBG) debug("stopSearch()"); + mHandler.post(new Runnable() { + public void run() { + performStopSearch(); + } + }); + } + + /** + * Cancels the search dialog. Must be called from the service UI thread. + */ + /*package*/ void performStopSearch() { + if (DBG) debug("performStopSearch()"); + mSearchDialog.cancel(); + } + + /** + * Determines if the Search UI is currently displayed. + * + * @see SearchManager#isVisible() + */ + public boolean isVisible() { + return postAndWait(mIsShowing, false, "isShowing()"); + } + + private final Callable<Boolean> mIsShowing = new Callable<Boolean>() { + public Boolean call() { + return mSearchDialog.isShowing(); + } + }; + + public Bundle onSaveInstanceState() { + return postAndWait(mOnSaveInstanceState, null, "onSaveInstanceState()"); + } + + private final Callable<Bundle> mOnSaveInstanceState = new Callable<Bundle>() { + public Bundle call() { + if (mSearchDialog.isShowing()) { + return mSearchDialog.onSaveInstanceState(); + } else { + return null; + } + } + }; + + public void onRestoreInstanceState(final Bundle searchDialogState) { + if (searchDialogState != null) { + mHandler.post(new Runnable() { + public void run() { + mSearchDialog.onRestoreInstanceState(searchDialogState); + } + }); + } + } + + public void onConfigurationChanged(final Configuration newConfig) { + mHandler.post(new Runnable() { + public void run() { + if (mSearchDialog.isShowing()) { + mSearchDialog.onConfigurationChanged(newConfig); + } + } + }); + } + + /** + * Called by {@link SearchDialog} when it goes away. + */ + public void onDismiss(DialogInterface dialog) { + if (DBG) debug("onDismiss()"); + if (mCallback != null) { + try { + mCallback.onDismiss(); + } catch (RemoteException ex) { + Log.e(TAG, "onDismiss() failed: " + ex); + } + } + } + + /** + * Called by {@link SearchDialog} when the user or activity cancels search. + * When this is called, {@link #onDismiss} is called too. + */ + public void onCancel(DialogInterface dialog) { + if (DBG) debug("onCancel()"); + if (mCallback != null) { + try { + mCallback.onCancel(); + } catch (RemoteException ex) { + Log.e(TAG, "onCancel() failed: " + ex); + } + } + } + + /** + * Returns a list of the searchable activities that handle web searches. + */ + public List<SearchableInfo> getSearchablesForWebSearch() { + updateSearchablesIfDirty(); + return mSearchables.getSearchablesForWebSearchList(); + } + + /** + * Returns the default searchable activity for web searches. + */ + public SearchableInfo getDefaultSearchableForWebSearch() { + updateSearchablesIfDirty(); + return mSearchables.getDefaultSearchableForWebSearch(); + } + + /** + * Sets the default searchable activity for web searches. + */ + public void setDefaultWebSearch(ComponentName component) { + mSearchables.setDefaultWebSearch(component); + } + + /** + * Runs an operation on the handler for the service, blocks until it returns, + * and returns the value returned by the operation. + * + * @param <V> Return value type. + * @param callable Operation to run. + * @param errorResult Value to return if the operations throws an exception. + * @param name Operation name to include in error log messages. + * @return The value returned by the operation. + */ + private <V> V postAndWait(Callable<V> callable, V errorResult, String name) { + FutureTask<V> task = new FutureTask<V>(callable); + mHandler.post(task); + try { + return task.get(); + } catch (InterruptedException ex) { + Log.e(TAG, "Error calling " + name + ": " + ex); + return errorResult; + } catch (ExecutionException ex) { + Log.e(TAG, "Error calling " + name + ": " + ex); + return errorResult; + } + } + + private static void debug(String msg) { + Thread thread = Thread.currentThread(); + Log.d(TAG, msg + " (" + thread.getName() + "-" + thread.getId() + ")"); + } } diff --git a/core/java/android/server/search/SearchableInfo.java b/core/java/android/server/search/SearchableInfo.java index 842fc75..8ef1f15 100644 --- a/core/java/android/server/search/SearchableInfo.java +++ b/core/java/android/server/search/SearchableInfo.java @@ -40,7 +40,7 @@ import java.util.HashMap; public final class SearchableInfo implements Parcelable { // general debugging support - private static final boolean DBG = true; + private static final boolean DBG = false; private static final String LOG_TAG = "SearchableInfo"; // static strings used for XML lookups. @@ -66,6 +66,8 @@ public final class SearchableInfo implements Parcelable { private final int mSearchInputType; private final int mSearchImeOptions; private final boolean mIncludeInGlobalSearch; + private final boolean mQueryAfterZeroResults; + private final String mSettingsDescription; private final String mSuggestAuthority; private final String mSuggestPath; private final String mSuggestSelection; @@ -133,6 +135,14 @@ public final class SearchableInfo implements Parcelable { public boolean shouldRewriteQueryFromText() { return 0 != (mSearchMode & SEARCH_MODE_QUERY_REWRITE_FROM_TEXT); } + + /** + * Gets the description to use for this source in system search settings, or null if + * none has been specified. + */ + public String getSettingsDescription() { + return mSettingsDescription; + } /** * Retrieve the path for obtaining search suggestions. @@ -276,7 +286,11 @@ public final class SearchableInfo implements Parcelable { EditorInfo.IME_ACTION_SEARCH); mIncludeInGlobalSearch = a.getBoolean( com.android.internal.R.styleable.Searchable_includeInGlobalSearch, false); + mQueryAfterZeroResults = a.getBoolean( + com.android.internal.R.styleable.Searchable_queryAfterZeroResults, false); + mSettingsDescription = a.getString( + com.android.internal.R.styleable.Searchable_searchSettingsDescription); mSuggestAuthority = a.getString( com.android.internal.R.styleable.Searchable_searchSuggestAuthority); mSuggestPath = a.getString( @@ -317,7 +331,7 @@ public final class SearchableInfo implements Parcelable { // for now, implement some form of rules - minimal data if (mLabelId == 0) { - throw new IllegalArgumentException("No label."); + throw new IllegalArgumentException("Search label must be a resource reference."); } } @@ -438,13 +452,18 @@ public final class SearchableInfo implements Parcelable { xml.close(); if (DBG) { - Log.d(LOG_TAG, "Checked " + activityInfo.name - + ",label=" + searchable.getLabelId() - + ",icon=" + searchable.getIconId() - + ",suggestAuthority=" + searchable.getSuggestAuthority() - + ",target=" + searchable.getSearchActivity().getClassName() - + ",global=" + searchable.shouldIncludeInGlobalSearch() - + ",threshold=" + searchable.getSuggestThreshold()); + if (searchable != null) { + Log.d(LOG_TAG, "Checked " + activityInfo.name + + ",label=" + searchable.getLabelId() + + ",icon=" + searchable.getIconId() + + ",suggestAuthority=" + searchable.getSuggestAuthority() + + ",target=" + searchable.getSearchActivity().getClassName() + + ",global=" + searchable.shouldIncludeInGlobalSearch() + + ",settingsDescription=" + searchable.getSettingsDescription() + + ",threshold=" + searchable.getSuggestThreshold()); + } else { + Log.d(LOG_TAG, "Checked " + activityInfo.name + ", no searchable meta-data"); + } } return searchable; } @@ -637,6 +656,17 @@ public final class SearchableInfo implements Parcelable { } /** + * Checks whether this searchable activity should be invoked after a query returned zero + * results. + * + * @return The value of the <code>queryAfterZeroResults</code> attribute, + * or <code>false</code> if the attribute is not set. + */ + public boolean queryAfterZeroResults() { + return mQueryAfterZeroResults; + } + + /** * Support for parcelable and aidl operations. */ public static final Parcelable.Creator<SearchableInfo> CREATOR @@ -667,7 +697,9 @@ public final class SearchableInfo implements Parcelable { mSearchInputType = in.readInt(); mSearchImeOptions = in.readInt(); mIncludeInGlobalSearch = in.readInt() != 0; - + mQueryAfterZeroResults = in.readInt() != 0; + + mSettingsDescription = in.readString(); mSuggestAuthority = in.readString(); mSuggestPath = in.readString(); mSuggestSelection = in.readString(); @@ -702,7 +734,9 @@ public final class SearchableInfo implements Parcelable { dest.writeInt(mSearchInputType); dest.writeInt(mSearchImeOptions); dest.writeInt(mIncludeInGlobalSearch ? 1 : 0); + dest.writeInt(mQueryAfterZeroResults ? 1 : 0); + dest.writeString(mSettingsDescription); dest.writeString(mSuggestAuthority); dest.writeString(mSuggestPath); dest.writeString(mSuggestSelection); diff --git a/core/java/android/server/search/Searchables.java b/core/java/android/server/search/Searchables.java index 9586d56..c7cc8ed 100644 --- a/core/java/android/server/search/Searchables.java +++ b/core/java/android/server/search/Searchables.java @@ -16,49 +16,64 @@ package android.server.search; +import com.android.internal.app.ResolverActivity; +import com.android.internal.R; + import android.app.SearchManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.res.Resources; import android.os.Bundle; +import android.util.Log; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** - * This class maintains the information about all searchable activities. + * This class maintains the information about all searchable activities. */ public class Searchables { + private static final String LOG_TAG = "Searchables"; + // static strings used for XML lookups, etc. - // TODO how should these be documented for the developer, in a more structured way than + // TODO how should these be documented for the developer, in a more structured way than // the current long wordy javadoc in SearchManager.java ? private static final String MD_LABEL_DEFAULT_SEARCHABLE = "android.app.default_searchable"; private static final String MD_SEARCHABLE_SYSTEM_SEARCH = "*"; - + private Context mContext; - + private HashMap<ComponentName, SearchableInfo> mSearchablesMap = null; private ArrayList<SearchableInfo> mSearchablesList = null; private ArrayList<SearchableInfo> mSearchablesInGlobalSearchList = null; + private ArrayList<SearchableInfo> mSearchablesForWebSearchList = null; private SearchableInfo mDefaultSearchable = null; - + private SearchableInfo mDefaultSearchableForWebSearch = null; + + public static String GOOGLE_SEARCH_COMPONENT_NAME = + "com.android.googlesearch/.GoogleSearch"; + public static String ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME = + "com.google.android.providers.enhancedgooglesearch/.Launcher"; + /** - * + * * @param context Context to use for looking up activities etc. */ public Searchables (Context context) { mContext = context; } - + /** * Look up, or construct, based on the activity. - * - * The activities fall into three cases, based on meta-data found in + * + * The activities fall into three cases, based on meta-data found in * the manifest entry: * <ol> * <li>The activity itself implements search. This is indicated by the @@ -70,16 +85,16 @@ public class Searchables { * case the factory will "redirect" and return the searchable data.</li> * <li>No searchability data is provided. We return null here and other * code will insert the "default" (e.g. contacts) search. - * + * * TODO: cache the result in the map, and check the map first. * TODO: it might make sense to implement the searchable reference as * an application meta-data entry. This way we don't have to pepper each * and every activity. * TODO: can we skip the constructor step if it's a non-searchable? - * TODO: does it make sense to plug the default into a slot here for + * TODO: does it make sense to plug the default into a slot here for * automatic return? Probably not, but it's one way to do it. * - * @param activity The name of the current activity, or null if the + * @param activity The name of the current activity, or null if the * activity does not define any explicit searchable metadata. */ public SearchableInfo getSearchableInfo(ComponentName activity) { @@ -89,18 +104,18 @@ public class Searchables { result = mSearchablesMap.get(activity); if (result != null) return result; } - + // Step 2. See if the current activity references a searchable. // Note: Conceptually, this could be a while(true) loop, but there's - // no point in implementing reference chaining here and risking a loop. + // no point in implementing reference chaining here and risking a loop. // References must point directly to searchable activities. - + ActivityInfo ai = null; try { ai = mContext.getPackageManager(). getActivityInfo(activity, PackageManager.GET_META_DATA ); String refActivityName = null; - + // First look for activity-specific reference Bundle md = ai.metaData; if (md != null) { @@ -113,11 +128,11 @@ public class Searchables { refActivityName = md.getString(MD_LABEL_DEFAULT_SEARCHABLE); } } - + // Irrespective of source, if a reference was found, follow it. if (refActivityName != null) { - // An app or activity can declare that we should simply launch + // An app or activity can declare that we should simply launch // "system default search" if search is invoked. if (refActivityName.equals(MD_SEARCHABLE_SYSTEM_SEARCH)) { return getDefaultSearchable(); @@ -143,95 +158,212 @@ public class Searchables { } catch (PackageManager.NameNotFoundException e) { // case 3: no metadata } - + // Step 3. None found. Return null. return null; - + } - + /** * Provides the system-default search activity, which you can use * whenever getSearchableInfo() returns null; - * + * * @return Returns the system-default search activity, null if never defined */ public synchronized SearchableInfo getDefaultSearchable() { return mDefaultSearchable; } - + public synchronized boolean isDefaultSearchable(SearchableInfo searchable) { return searchable == mDefaultSearchable; } - + /** - * Builds an entire list (suitable for display) of - * activities that are searchable, by iterating the entire set of - * ACTION_SEARCH intents. - * + * Builds an entire list (suitable for display) of + * activities that are searchable, by iterating the entire set of + * ACTION_SEARCH & ACTION_WEB_SEARCH intents. + * * Also clears the hash of all activities -> searches which will * refill as the user clicks "search". - * + * * This should only be done at startup and again if we know that the * list has changed. - * + * * TODO: every activity that provides a ACTION_SEARCH intent should * also provide searchability meta-data. There are a bunch of checks here * that, if data is not found, silently skip to the next activity. This * won't help a developer trying to figure out why their activity isn't * showing up in the list, but an exception here is too rough. I would * like to find a better notification mechanism. - * + * * TODO: sort the list somehow? UI choice. */ public void buildSearchableList() { - // These will become the new values at the end of the method - HashMap<ComponentName, SearchableInfo> newSearchablesMap + HashMap<ComponentName, SearchableInfo> newSearchablesMap = new HashMap<ComponentName, SearchableInfo>(); ArrayList<SearchableInfo> newSearchablesList = new ArrayList<SearchableInfo>(); ArrayList<SearchableInfo> newSearchablesInGlobalSearchList = new ArrayList<SearchableInfo>(); + ArrayList<SearchableInfo> newSearchablesForWebSearchList + = new ArrayList<SearchableInfo>(); final PackageManager pm = mContext.getPackageManager(); - - // use intent resolver to generate list of ACTION_SEARCH receivers - List<ResolveInfo> infoList; + + // Use intent resolver to generate list of ACTION_SEARCH & ACTION_WEB_SEARCH receivers. + List<ResolveInfo> searchList; final Intent intent = new Intent(Intent.ACTION_SEARCH); - infoList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA); - + searchList = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA); + + List<ResolveInfo> webSearchInfoList; + final Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH); + webSearchInfoList = pm.queryIntentActivities(webSearchIntent, PackageManager.GET_META_DATA); + // analyze each one, generate a Searchables record, and record - if (infoList != null) { - int count = infoList.size(); + if (searchList != null || webSearchInfoList != null) { + int search_count = (searchList == null ? 0 : searchList.size()); + int web_search_count = (webSearchInfoList == null ? 0 : webSearchInfoList.size()); + int count = search_count + web_search_count; for (int ii = 0; ii < count; ii++) { // for each component, try to find metadata - ResolveInfo info = infoList.get(ii); + ResolveInfo info = (ii < search_count) + ? searchList.get(ii) + : webSearchInfoList.get(ii - search_count); ActivityInfo ai = info.activityInfo; - SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai); - if (searchable != null) { - newSearchablesList.add(searchable); - newSearchablesMap.put(searchable.getSearchActivity(), searchable); - if (searchable.shouldIncludeInGlobalSearch()) { - newSearchablesInGlobalSearchList.add(searchable); + // Check first to avoid duplicate entries. + if (newSearchablesMap.get(new ComponentName(ai.packageName, ai.name)) == null) { + SearchableInfo searchable = SearchableInfo.getActivityMetaData(mContext, ai); + if (searchable != null) { + newSearchablesList.add(searchable); + newSearchablesMap.put(searchable.getSearchActivity(), searchable); + if (searchable.shouldIncludeInGlobalSearch()) { + newSearchablesInGlobalSearchList.add(searchable); + } } } } } - + + if (webSearchInfoList != null) { + for (int i = 0; i < webSearchInfoList.size(); ++i) { + ActivityInfo ai = webSearchInfoList.get(i).activityInfo; + ComponentName component = new ComponentName(ai.packageName, ai.name); + newSearchablesForWebSearchList.add(newSearchablesMap.get(component)); + } + } + // Find the global search provider Intent globalSearchIntent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH); ComponentName globalSearchActivity = globalSearchIntent.resolveActivity(pm); SearchableInfo newDefaultSearchable = newSearchablesMap.get(globalSearchActivity); + if (newDefaultSearchable == null) { + Log.w(LOG_TAG, "No searchable info found for new default searchable activity " + + globalSearchActivity); + } + + // Find the default web search provider. + ComponentName webSearchActivity = getPreferredWebSearchActivity(); + SearchableInfo newDefaultSearchableForWebSearch = null; + if (webSearchActivity != null) { + newDefaultSearchableForWebSearch = newSearchablesMap.get(webSearchActivity); + } + if (newDefaultSearchableForWebSearch == null) { + Log.w(LOG_TAG, "No searchable info found for new default web search activity " + + webSearchActivity); + } + // Store a consistent set of new values synchronized (this) { mSearchablesMap = newSearchablesMap; mSearchablesList = newSearchablesList; mSearchablesInGlobalSearchList = newSearchablesInGlobalSearchList; + mSearchablesForWebSearchList = newSearchablesForWebSearchList; mDefaultSearchable = newDefaultSearchable; + mDefaultSearchableForWebSearch = newDefaultSearchableForWebSearch; + } + + // Inform all listeners that the list of searchables has been updated. + mContext.sendBroadcast(new Intent(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED)); + } + + /** + * Checks if the given activity component is present in the system and if so makes it the + * preferred activity for handling ACTION_WEB_SEARCH. + * @param component Name of the component to check and set as preferred. + * @param action Intent action for which this activity is to be set as preferred. + * @return true if component was detected and set as preferred activity, false if not. + */ + private boolean setPreferredActivity(ComponentName component, String action) { + Log.d(LOG_TAG, "Checking component " + component); + PackageManager pm = mContext.getPackageManager(); + ActivityInfo ai; + try { + ai = pm.getActivityInfo(component, 0); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + + // The code here to find the value for bestMatch is heavily inspired by the code + // in ResolverActivity where the preferred activity is set. + Intent intent = new Intent(action); + intent.addCategory(Intent.CATEGORY_DEFAULT); + List<ResolveInfo> webSearchActivities = pm.queryIntentActivities(intent, 0); + ComponentName set[] = new ComponentName[webSearchActivities.size()]; + int bestMatch = 0; + for (int i = 0; i < webSearchActivities.size(); ++i) { + ResolveInfo ri = webSearchActivities.get(i); + set[i] = new ComponentName(ri.activityInfo.packageName, + ri.activityInfo.name); + if (ri.match > bestMatch) bestMatch = ri.match; + } + + Log.d(LOG_TAG, "Setting preferred web search activity to " + component); + IntentFilter filter = new IntentFilter(action); + filter.addCategory(Intent.CATEGORY_DEFAULT); + pm.replacePreferredActivity(filter, bestMatch, set, component); + return true; + } + + public ComponentName getPreferredWebSearchActivity() { + // Check if we have a preferred web search activity. + Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); + PackageManager pm = mContext.getPackageManager(); + ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY); + + if (ri == null || ri.activityInfo.name.equals(ResolverActivity.class.getName())) { + Log.d(LOG_TAG, "No preferred activity set for action web search."); + + // The components in the providers array are checked in the order of declaration so the + // first one has the highest priority. If the component exists in the system it is set + // as the preferred activity to handle intent action web search. + String[] preferredActivities = mContext.getResources().getStringArray( + com.android.internal.R.array.default_web_search_providers); + for (String componentName : preferredActivities) { + ComponentName component = ComponentName.unflattenFromString(componentName); + if (setPreferredActivity(component, Intent.ACTION_WEB_SEARCH)) { + return component; + } + } + } else { + // If the current preferred activity is GoogleSearch, and we detect + // EnhancedGoogleSearch installed as well, set the latter as preferred since that + // is a superset and provides more functionality. + ComponentName cn = new ComponentName(ri.activityInfo.packageName, ri.activityInfo.name); + if (cn.flattenToShortString().equals(GOOGLE_SEARCH_COMPONENT_NAME)) { + ComponentName enhancedGoogleSearch = ComponentName.unflattenFromString( + ENHANCED_GOOGLE_SEARCH_COMPONENT_NAME); + if (setPreferredActivity(enhancedGoogleSearch, Intent.ACTION_WEB_SEARCH)) { + return enhancedGoogleSearch; + } + } } + + if (ri == null) return null; + return new ComponentName(ri.activityInfo.packageName, ri.activityInfo.name); } - + /** * Returns the list of searchable activities. */ @@ -239,11 +371,33 @@ public class Searchables { ArrayList<SearchableInfo> result = new ArrayList<SearchableInfo>(mSearchablesList); return result; } - + /** * Returns a list of the searchable activities that can be included in global search. */ public synchronized ArrayList<SearchableInfo> getSearchablesInGlobalSearchList() { return new ArrayList<SearchableInfo>(mSearchablesInGlobalSearchList); } + + /** + * Returns a list of the searchable activities that handle web searches. + */ + public synchronized ArrayList<SearchableInfo> getSearchablesForWebSearchList() { + return new ArrayList<SearchableInfo>(mSearchablesForWebSearchList); + } + + /** + * Returns the default searchable activity for web searches. + */ + public synchronized SearchableInfo getDefaultSearchableForWebSearch() { + return mDefaultSearchableForWebSearch; + } + + /** + * Sets the default searchable activity for web searches. + */ + public synchronized void setDefaultWebSearch(ComponentName component) { + setPreferredActivity(component, Intent.ACTION_WEB_SEARCH); + buildSearchableList(); + } } diff --git a/core/java/android/speech/IRecognitionListener.aidl b/core/java/android/speech/IRecognitionListener.aidl index 6ed32b5..2da2258 100644 --- a/core/java/android/speech/IRecognitionListener.aidl +++ b/core/java/android/speech/IRecognitionListener.aidl @@ -17,6 +17,7 @@ package android.speech; import android.os.Bundle; +import android.speech.RecognitionResult; /** * Listener for speech recognition events, used with RecognitionService. @@ -43,13 +44,17 @@ interface IRecognitionListener { /** Called after the user stops speaking. */ void onEndOfSpeech(); - /** A network or recognition error occurred. */ - void onError(in String error); + /** + * A network or recognition error occurred. The code is defined in + * {@link android.speech.RecognitionResult} + */ + void onError(in int error); /** - * Called when recognition transcripts are ready. - * results: an ordered list of the most likely transcripts (N-best list). - * @hide + * Called when recognition results are ready. + * @param results: an ordered list of the most likely results (N-best list). + * @param key: a key associated with the results. The same results can + * be retrieved asynchronously later using the key, if available. */ - void onResults(in List<String> results); + void onResults(in List<RecognitionResult> results, long key); } diff --git a/core/java/android/speech/IRecognitionService.aidl b/core/java/android/speech/IRecognitionService.aidl index 36d12e9a..a18c380 100644 --- a/core/java/android/speech/IRecognitionService.aidl +++ b/core/java/android/speech/IRecognitionService.aidl @@ -18,6 +18,7 @@ package android.speech; import android.content.Intent; import android.speech.IRecognitionListener; +import android.speech.RecognitionResult; // A Service interface to speech recognition. Call startListening when // you want to begin capturing audio; RecognitionService will automatically @@ -29,6 +30,8 @@ interface IRecognitionService { // see RecognizerIntent.java for constants used to specify the intent. void startListening(in Intent recognizerIntent, in IRecognitionListener listener); + + List<RecognitionResult> getRecognitionResults(in long key); void cancel(); } diff --git a/core/java/android/speech/RecognitionResult.aidl b/core/java/android/speech/RecognitionResult.aidl new file mode 100644 index 0000000..59e53ab --- /dev/null +++ b/core/java/android/speech/RecognitionResult.aidl @@ -0,0 +1,19 @@ +/* + * 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; + +parcelable RecognitionResult; diff --git a/core/java/android/speech/RecognitionResult.java b/core/java/android/speech/RecognitionResult.java new file mode 100644 index 0000000..8d031fc --- /dev/null +++ b/core/java/android/speech/RecognitionResult.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * 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; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * RecognitionResult is a passive object that stores a single recognized + * query and its search result. + * TODO: revisit and improve. May be we should have a separate result + * object for each type, and put them (type/value) in bundle? + * + * {@hide} + */ +public class RecognitionResult implements Parcelable { + /** + * Status of the recognize request. + */ + public static final int NETWORK_TIMEOUT = 1; // Network operation timed out. + public static final int NETWORK_ERROR = 2; // Other networkrelated errors. + public static final int AUDIO_ERROR = 3; // Audio recording error. + public static final int SERVER_ERROR = 4; // Server sends error status. + public static final int CLIENT_ERROR = 5; // Other client side errors. + public static final int SPEECH_TIMEOUT = 6; // No speech input + public static final int NO_MATCH = 7; // No recognition result matched. + public static final int SERVICE_BUSY = 8; // RecognitionService busy. + + /** + * Type of the recognition results. + */ + public static final int RAW_RECOGNITION_RESULT = 0; + public static final int WEB_SEARCH_RESULT = 1; + public static final int CONTACT_RESULT = 2; + + /** + * A factory method to create a raw RecognitionResult + * + * @param sentence the recognized text. + */ + public static RecognitionResult newRawRecognitionResult(String sentence) { + return new RecognitionResult(RAW_RECOGNITION_RESULT, sentence, null, null); + } + + /** + * A factory method to create RecognitionResult for contacts. + * + * @param contact the contact name. + * @param phoneType the phone type. + */ + public static RecognitionResult newContactResult(String contact, int phoneType) { + return new RecognitionResult(CONTACT_RESULT, contact, phoneType); + } + + /** + * A factory method to create a RecognitionResult for Web Search Query. + * + * @param query the query string. + * @param html the html page of the search result. + * @param url the url that performs the search with the query. + */ + public static RecognitionResult newWebResult(String query, String html, String url) { + return new RecognitionResult(WEB_SEARCH_RESULT, query, html, url); + } + + public static final Parcelable.Creator<RecognitionResult> CREATOR + = new Parcelable.Creator<RecognitionResult>() { + + public RecognitionResult createFromParcel(Parcel in) { + return new RecognitionResult(in); + } + + public RecognitionResult[] newArray(int size) { + return new RecognitionResult[size]; + } + }; + + /** + * Result type. + */ + public final int mResultType; + + /** + * The recognized string when mResultType is WEB_SEARCH_RESULT. + * The name of the contact when mResultType is CONTACT_RESULT. + */ + public final String mText; + + /** + * The HTML result page for the query. If this is null, then the + * application must use the url field to get the HTML result page. + */ + public final String mHtml; + + /** + * The url to get the result page for the query string. The + * application must use this url instead of performing the search + * with the query. + */ + public final String mUrl; + + /** Phone number type. This is valid only when mResultType == CONTACT_RESULT */ + public final int mPhoneType; + + private RecognitionResult(int type, String query, String html, String url) { + mResultType = type; + mText = query; + mHtml = html; + mUrl = url; + mPhoneType = -1; + } + + private RecognitionResult(int type, String query, int at) { + mResultType = type; + mText = query; + mPhoneType = at; + mHtml = null; + mUrl = null; + } + + private RecognitionResult(Parcel in) { + mResultType = in.readInt(); + mText = in.readString(); + mHtml= in.readString(); + mUrl= in.readString(); + mPhoneType = in.readInt(); + } + + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mResultType); + out.writeString(mText); + out.writeString(mHtml); + out.writeString(mUrl); + out.writeInt(mPhoneType); + } + + + @Override + public String toString() { + String resultType[] = { "RAW", "WEB", "CONTACT" }; + return "[type=" + resultType[mResultType] + + ", text=" + mText+ ", mUrl=" + mUrl + ", html=" + mHtml + "]"; + } + + public int describeContents() { + // no special description + return 0; + } +} diff --git a/core/java/android/speech/RecognitionServiceUtil.java b/core/java/android/speech/RecognitionServiceUtil.java index 650c0fd..a8c7868 100644 --- a/core/java/android/speech/RecognitionServiceUtil.java +++ b/core/java/android/speech/RecognitionServiceUtil.java @@ -21,6 +21,9 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; +import android.os.RemoteException; +import android.speech.RecognitionResult; +import android.util.Log; import java.util.List; @@ -56,6 +59,11 @@ public class RecognitionServiceUtil { public static final Intent sDefaultIntent = new Intent( RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + // Recognize request parameters + public static final String USE_LOCATION = "useLocation"; + public static final String CONTACT_AUTH_TOKEN = "contactAuthToken"; + + // Bundles public static final String NOISE_LEVEL = "NoiseLevel"; public static final String SIGNAL_NOISE_RATIO = "SignalNoiseRatio"; @@ -72,8 +80,8 @@ public class RecognitionServiceUtil { public void onRmsChanged(float rmsdB) {} public void onBufferReceived(byte[] buf) {} public void onEndOfSpeech() {} - public void onError(String error) {} - public void onResults(List<String> results) {} + public void onError(int error) {} + public void onResults(List<RecognitionResult> results, long key) {} } /** diff --git a/core/java/android/speech/tts/ITts.aidl b/core/java/android/speech/tts/ITts.aidl new file mode 100755 index 0000000..c9a6180 --- /dev/null +++ b/core/java/android/speech/tts/ITts.aidl @@ -0,0 +1,63 @@ +/*
+ * 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 int speechRate);
+
+ int setPitch(in int pitch);
+
+ int speak(in String text, in int queueMode, in String[] params);
+
+ boolean isSpeaking();
+
+ int stop();
+
+ void addSpeech(in String text, in String packageName, in int resId);
+
+ void addSpeechFile(in String text, in String filename);
+
+ String[] getLanguage();
+
+ int isLanguageAvailable(in String language, in String country, in String variant);
+
+ int setLanguage(in String language, in String country, in String variant);
+
+ boolean synthesizeToFile(in String text, in String[] params, in String outputDirectory);
+
+ int playEarcon(in String earcon, in int queueMode, in String[] params);
+
+ void addEarcon(in String earcon, in String packageName, in int resId);
+
+ void addEarconFile(in String earcon, in String filename);
+
+ void registerCallback(ITtsCallback cb);
+
+ void unregisterCallback(ITtsCallback cb);
+
+ int playSilence(in long duration, in int queueMode, in String[] params);
+}
diff --git a/core/java/android/speech/tts/ITtsCallback.aidl b/core/java/android/speech/tts/ITtsCallback.aidl new file mode 100755 index 0000000..48ed73e --- /dev/null +++ b/core/java/android/speech/tts/ITtsCallback.aidl @@ -0,0 +1,27 @@ +/* + * 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; + +/** + * AIDL for the callback from the TTS Service + * ITtsCallback.java is autogenerated from this. + * + * {@hide} + */ +oneway interface ITtsCallback { + void markReached(String mark); +} diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java new file mode 100644 index 0000000..616b3f1 --- /dev/null +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -0,0 +1,719 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * 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.ITts; +import android.speech.tts.ITtsCallback; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import java.util.HashMap; +import java.util.Locale; + +/** + * + * Synthesizes speech from text for immediate playback or to create a sound file. + * + */ +//TODO complete javadoc + add links to constants +public class TextToSpeech { + + /** + * Denotes a successful operation. + */ + public static final int TTS_SUCCESS = 0; + /** + * Denotes a generic operation failure. + */ + public static final int TTS_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. + */ + public static final int TTS_QUEUE_FLUSH = 0; + /** + * Queue mode where the new entry is added at the end of the playback queue. + */ + public static final int TTS_QUEUE_ADD = 1; + + + /** + * Denotes the language is available exactly as specified by the locale + */ + public static final int TTS_LANG_COUNTRY_VAR_AVAILABLE = 2; + + + /** + * Denotes the language is available for the language and country specified + * by the locale, but not the variant. + */ + public static final int TTS_LANG_COUNTRY_AVAILABLE = 1; + + + /** + * Denotes the language is available for the language by the locale, + * but not the country and variant. + */ + public static final int TTS_LANG_AVAILABLE = 0; + + /** + * Denotes the language data is missing. + */ + public static final int TTS_LANG_MISSING_DATA = -1; + + /** + * Denotes the language is not supported by the current TTS engine. + */ + public static final int TTS_LANG_NOT_SUPPORTED = -2; + + + /** + * Called when the TTS has initialized. + * + * The InitListener must implement the onInit function. onInit is passed a + * status code indicating the result of the TTS initialization. + */ + public interface OnInitListener { + public void onInit(int status); + } + + /** + * Internal constants for the TTS functionality + * + * {@hide} + */ + public class Engine { + // default values for a TTS engine when settings are not found in the provider + public static final int FALLBACK_TTS_DEFAULT_RATE = 100; // 1x + public static final int FALLBACK_TTS_DEFAULT_PITCH = 100;// 1x + public static final int FALLBACK_TTS_USE_DEFAULTS = 0; // false + public static final String FALLBACK_TTS_DEFAULT_LANG = "eng"; + public static final String FALLBACK_TTS_DEFAULT_COUNTRY = ""; + public static final String FALLBACK_TTS_DEFAULT_VARIANT = ""; + + // return codes for a TTS engine's check data activity + public static final int CHECK_VOICE_DATA_PASS = 1; + public static final int CHECK_VOICE_DATA_FAIL = 0; + public static final int CHECK_VOICE_DATA_BAD_DATA = -1; + public static final int CHECK_VOICE_DATA_MISSING_DATA = -2; + public static final int CHECK_VOICE_DATA_MISSING_DATA_NO_SDCARD = -3; + + // return codes for a TTS engine's check data activity + public static final String VOICE_DATA_ROOT_DIRECTORY = "dataRoot"; + public static final String VOICE_DATA_FILES = "dataFiles"; + public static final String VOICE_DATA_FILES_INFO = "dataFilesInfo"; + + // keys for the parameters passed with speak commands + public static final String TTS_KEY_PARAM_RATE = "rate"; + public static final String TTS_KEY_PARAM_LANGUAGE = "language"; + public static final String TTS_KEY_PARAM_COUNTRY = "country"; + public static final String TTS_KEY_PARAM_VARIANT = "variant"; + public static final int TTS_PARAM_POSITION_RATE = 0; + public static final int TTS_PARAM_POSITION_LANGUAGE = 2; + public static final int TTS_PARAM_POSITION_COUNTRY = 4; + public static final int TTS_PARAM_POSITION_VARIANT = 6; + } + + /** + * Connection needed for the TTS. + */ + private ServiceConnection mServiceConnection; + + private ITts mITts = null; + private Context mContext = null; + private OnInitListener mInitListener = null; + private boolean mStarted = false; + private final Object mStartLock = new Object(); + /** + * Used to store the cached parameters sent along with each synthesis request to the + * TTS service. + */ + private String[] mCachedParams; + + /** + * The constructor for the TTS. + * + * @param context + * The context + * @param listener + * The InitListener that will be called when the TTS has + * initialized successfully. + */ + public TextToSpeech(Context context, OnInitListener listener) { + mContext = context; + mInitListener = listener; + + mCachedParams = new String[2*4]; // 4 parameters, store key and value + mCachedParams[Engine.TTS_PARAM_POSITION_RATE] = Engine.TTS_KEY_PARAM_RATE; + mCachedParams[Engine.TTS_PARAM_POSITION_LANGUAGE] = Engine.TTS_KEY_PARAM_LANGUAGE; + mCachedParams[Engine.TTS_PARAM_POSITION_COUNTRY] = Engine.TTS_KEY_PARAM_COUNTRY; + mCachedParams[Engine.TTS_PARAM_POSITION_VARIANT] = Engine.TTS_KEY_PARAM_VARIANT; + + mCachedParams[Engine.TTS_PARAM_POSITION_RATE + 1] = + String.valueOf(Engine.FALLBACK_TTS_DEFAULT_RATE); + // initialize the language cached parameters with the current Locale + Locale defaultLoc = Locale.getDefault(); + mCachedParams[Engine.TTS_PARAM_POSITION_LANGUAGE + 1] = defaultLoc.getISO3Language(); + mCachedParams[Engine.TTS_PARAM_POSITION_COUNTRY + 1] = defaultLoc.getISO3Country(); + mCachedParams[Engine.TTS_PARAM_POSITION_VARIANT + 1] = defaultLoc.getVariant(); + + initTts(); + } + + + 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; + if (mInitListener != null) { + // TODO manage failures and missing resources + mInitListener.onInit(TTS_SUCCESS); + } + } + } + + public void onServiceDisconnected(ComponentName name) { + synchronized(mStartLock) { + mITts = null; + mInitListener = null; + mStarted = false; + } + } + }; + + Intent intent = new Intent("android.intent.action.START_TTS_SERVICE"); + intent.addCategory("android.intent.category.TTS"); + mContext.bindService(intent, mServiceConnection, + Context.BIND_AUTO_CREATE); + // TODO handle case where the binding works (should always work) but + // the plugin fails + } + + + /** + * Shuts down the TTS. It is good practice to call this in the onDestroy + * method of the Activity that is using the TTS so that the TTS is stopped + * cleanly. + */ + 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. + } + } + + + /** + * Adds a mapping between a string of text and a sound resource in a + * package. + * + * @see #TTS.speak(String text, int queueMode, String[] params) + * + * @param text + * Example: <b><code>"south_south_east"</code></b><br/> + * + * @param packagename + * Pass the packagename of the application that contains the + * resource. If the resource is in your own application (this is + * the most common case), then put the packagename of your + * application here.<br/> + * Example: <b>"com.google.marvin.compass"</b><br/> + * The packagename can be found in the AndroidManifest.xml of + * your application. + * <p> + * <code><manifest xmlns:android="..." + * package="<b>com.google.marvin.compass</b>"></code> + * </p> + * + * @param resourceId + * Example: <b><code>R.raw.south_south_east</code></b> + * + * @return Code indicating success or failure. See TTS_ERROR and TTS_SUCCESS. + */ + public int addSpeech(String text, String packagename, int resourceId) { + synchronized(mStartLock) { + if (!mStarted) { + return TTS_ERROR; + } + try { + mITts.addSpeech(text, packagename, resourceId); + return TTS_SUCCESS; + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (NullPointerException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (IllegalStateException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } + return TTS_ERROR; + } + } + + + /** + * Adds a mapping between a string of text and a sound file. Using this, it + * is possible to add custom pronounciations for text. + * + * @param text + * The string of text + * @param filename + * The full path to the sound file (for example: + * "/sdcard/mysounds/hello.wav") + * + * @return Code indicating success or failure. See TTS_ERROR and TTS_SUCCESS. + */ + public int addSpeech(String text, String filename) { + synchronized (mStartLock) { + if (!mStarted) { + return TTS_ERROR; + } + try { + mITts.addSpeechFile(text, filename); + return TTS_SUCCESS; + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (NullPointerException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (IllegalStateException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } + return TTS_ERROR; + } + } + + + /** + * Speaks the string using the specified queuing strategy and speech + * parameters. Note that the speech parameters are not universally supported + * by all engines and will be treated as a hint. The TTS library will try to + * fulfill these parameters as much as possible, but there is no guarantee + * that the voice used will have the properties specified. + * + * @param text + * The string of text to be spoken. + * @param queueMode + * The queuing strategy to use. + * See TTS_QUEUE_ADD and TTS_QUEUE_FLUSH. + * @param params + * The hashmap of speech parameters to be used. + * + * @return Code indicating success or failure. See TTS_ERROR and TTS_SUCCESS. + */ + public int speak(String text, int queueMode, HashMap<String,String> params) + { + synchronized (mStartLock) { + int result = TTS_ERROR; + Log.i("TTS received: ", text); + if (!mStarted) { + return result; + } + try { + // TODO support extra parameters, passing cache of current parameters for the moment + result = mITts.speak(text, queueMode, mCachedParams); + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (NullPointerException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (IllegalStateException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } finally { + return result; + } + } + } + + + /** + * Plays the earcon using the specified queueing mode and parameters. + * + * @param earcon + * The earcon that should be played + * @param queueMode + * See TTS_QUEUE_ADD and TTS_QUEUE_FLUSH. + * @param params + * The hashmap of parameters to be used. + * + * @return Code indicating success or failure. See TTS_ERROR and TTS_SUCCESS. + */ + public int playEarcon(String earcon, int queueMode, + HashMap<String,String> params) { + synchronized (mStartLock) { + int result = TTS_ERROR; + if (!mStarted) { + return result; + } + try { + // TODO support extra parameters, passing null for the moment + result = mITts.playEarcon(earcon, queueMode, null); + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (NullPointerException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (IllegalStateException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } finally { + return result; + } + } + } + + /** + * 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 + * See TTS_QUEUE_ADD and TTS_QUEUE_FLUSH. + * + * @return Code indicating success or failure. See TTS_ERROR and TTS_SUCCESS. + */ + public int playSilence(long durationInMs, int queueMode) { + synchronized (mStartLock) { + int result = TTS_ERROR; + if (!mStarted) { + return result; + } + try { + // TODO support extra parameters, passing cache of current parameters for the moment + result = mITts.playSilence(durationInMs, queueMode, mCachedParams); + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (NullPointerException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (IllegalStateException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } finally { + return result; + } + } + } + + + /** + * Returns whether or not the TTS is busy speaking. + * + * @return Whether or not the TTS is busy speaking. + */ + public boolean isSpeaking() { + synchronized (mStartLock) { + if (!mStarted) { + return false; + } + try { + return mITts.isSpeaking(); + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (NullPointerException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (IllegalStateException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } + return false; + } + } + + + /** + * Stops speech from the TTS. + * + * @return Code indicating success or failure. See TTS_ERROR and TTS_SUCCESS. + */ + public int stop() { + synchronized (mStartLock) { + int result = TTS_ERROR; + if (!mStarted) { + return result; + } + try { + result = mITts.stop(); + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (NullPointerException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (IllegalStateException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } finally { + return result; + } + } + } + + + /** + * Sets the speech rate for the TTS engine. + * + * Note that the speech rate is not universally supported by all engines and + * will be treated as a hint. The TTS library will try to use the specified + * speech rate, but there is no guarantee. + * This has no effect on any pre-recorded speech. + * + * @param speechRate + * The speech rate for the TTS 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). + * + * @return Code indicating success or failure. See TTS_ERROR and TTS_SUCCESS. + */ + public int setSpeechRate(float speechRate) { + synchronized (mStartLock) { + int result = TTS_ERROR; + if (!mStarted) { + return result; + } + try { + if (speechRate > 0) { + int rate = (int)(speechRate*100); + mCachedParams[Engine.TTS_PARAM_POSITION_RATE + 1] = String.valueOf(rate); + result = mITts.setSpeechRate(rate); + } + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } finally { + return result; + } + } + } + + + /** + * Sets the speech pitch for the TTS engine. + * + * Note that the pitch is not universally supported by all engines and + * will be treated as a hint. The TTS library will try to use the specified + * pitch, but there is no guarantee. + * This has no effect on any pre-recorded speech. + * + * @param pitch + * The pitch for the TTS engine. 1 is the normal pitch, + * lower values lower the tone of the synthesized voice, + * greater values increase it. + * + * @return Code indicating success or failure. See TTS_ERROR and TTS_SUCCESS. + */ + public int setPitch(float pitch) { + synchronized (mStartLock) { + int result = TTS_ERROR; + if (!mStarted) { + return result; + } + try { + if (pitch > 0) { + result = mITts.setPitch((int)(pitch*100)); + } + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } finally { + return result; + } + } + } + + + /** + * Sets the language for the TTS engine. + * + * Note that the language is not universally supported by all engines and + * will be treated as a hint. The TTS library will try to use the specified + * language as represented by the Locale, but there is no guarantee. + * + * @param loc + * The locale describing the language to be used. + * + * @return Code indicating the support status for the locale. See the TTS_LANG_ codes. + */ + public int setLanguage(Locale loc) { + synchronized (mStartLock) { + int result = TTS_LANG_NOT_SUPPORTED; + if (!mStarted) { + return result; + } + try { + mCachedParams[Engine.TTS_PARAM_POSITION_LANGUAGE + 1] = loc.getISO3Language(); + mCachedParams[Engine.TTS_PARAM_POSITION_COUNTRY + 1] = loc.getISO3Country(); + mCachedParams[Engine.TTS_PARAM_POSITION_VARIANT + 1] = loc.getVariant(); + result = mITts.setLanguage(mCachedParams[Engine.TTS_PARAM_POSITION_LANGUAGE + 1], + mCachedParams[Engine.TTS_PARAM_POSITION_COUNTRY + 1], + mCachedParams[Engine.TTS_PARAM_POSITION_VARIANT + 1] ); + // TTS died; restart it. + mStarted = false; + initTts(); + } finally { + return result; + } + } + } + + + /** + * Returns a Locale instance describing the language currently being used by the TTS engine. + * @return language, country (if any) and variant (if any) used by the engine stored in a Locale + * instance, or null is the TTS engine has failed. + */ + public Locale getLanguage() { + synchronized (mStartLock) { + if (!mStarted) { + return null; + } + try { + String[] locStrings = mITts.getLanguage(); + if (locStrings.length == 3) { + return new Locale(locStrings[0], locStrings[1], locStrings[2]); + } else { + return null; + } + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } + return null; + } + } + + /** + * Checks if the specified language as represented by the Locale is available. + * + * @param loc + * The Locale describing the language to be used. + * + * @return one of TTS_LANG_NOT_SUPPORTED, TTS_LANG_MISSING_DATA, TTS_LANG_AVAILABLE, + * TTS_LANG_COUNTRY_AVAILABLE, TTS_LANG_COUNTRY_VAR_AVAILABLE. + */ + public int isLanguageAvailable(Locale loc) { + synchronized (mStartLock) { + int result = TTS_LANG_NOT_SUPPORTED; + if (!mStarted) { + return result; + } + try { + result = mITts.isLanguageAvailable(loc.getISO3Language(), + loc.getISO3Country(), loc.getVariant()); + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } finally { + return result; + } + } + } + + + /** + * Synthesizes the given text to a file using the specified parameters. + * + * @param text + * The String of text that should be synthesized + * @param params + * A hashmap of parameters. + * @param filename + * The string that gives the full output filename; it should be + * something like "/sdcard/myappsounds/mysound.wav". + * + * @return Code indicating success or failure. See TTS_ERROR and TTS_SUCCESS. + */ + public int synthesizeToFile(String text, HashMap<String,String> params, + String filename) { + synchronized (mStartLock) { + int result = TTS_ERROR; + if (!mStarted) { + return result; + } + try { + // TODO support extra parameters, passing null for the moment + if (mITts.synthesizeToFile(text, null, filename)){ + result = TTS_SUCCESS; + } + } catch (RemoteException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (NullPointerException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } catch (IllegalStateException e) { + // TTS died; restart it. + mStarted = false; + initTts(); + } finally { + return result; + } + } + } + +} diff --git a/core/java/android/syncml/pim/PropertyNode.java b/core/java/android/syncml/pim/PropertyNode.java index cc52499..983ecb8 100644 --- a/core/java/android/syncml/pim/PropertyNode.java +++ b/core/java/android/syncml/pim/PropertyNode.java @@ -17,12 +17,16 @@ package android.syncml.pim; import android.content.ContentValues; -import android.util.Log; +import org.apache.commons.codec.binary.Base64; + +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.Map.Entry; +import java.util.regex.Pattern; public class PropertyNode { @@ -52,7 +56,9 @@ public class PropertyNode { public Set<String> propGroupSet; public PropertyNode() { + propName = ""; propValue = ""; + propValue_vector = new ArrayList<String>(); paramMap = new ContentValues(); paramMap_TYPE = new HashSet<String>(); propGroupSet = new HashSet<String>(); @@ -62,13 +68,21 @@ public class PropertyNode { String propName, String propValue, List<String> propValue_vector, byte[] propValue_bytes, ContentValues paramMap, Set<String> paramMap_TYPE, Set<String> propGroupSet) { - this.propName = propName; + if (propName != null) { + this.propName = propName; + } else { + this.propName = ""; + } if (propValue != null) { this.propValue = propValue; } else { this.propValue = ""; } - this.propValue_vector = propValue_vector; + if (propValue_vector != null) { + this.propValue_vector = propValue_vector; + } else { + this.propValue_vector = new ArrayList<String>(); + } this.propValue_bytes = propValue_bytes; if (paramMap != null) { this.paramMap = paramMap; @@ -117,17 +131,9 @@ public class PropertyNode { // decoded by BASE64 or QUOTED-PRINTABLE. When the size of propValue_vector // is 1, the encoded value is stored in propValue, so we do not have to // check it. - if (propValue_vector != null) { - // Log.d("@@@", "===" + propValue_vector + ", " + node.propValue_vector); - return (propValue_vector.equals(node.propValue_vector) || - (propValue_vector.size() == 1)); - } else if (node.propValue_vector != null) { - // Log.d("@@@", "===" + propValue_vector + ", " + node.propValue_vector); - return (node.propValue_vector.equals(propValue_vector) || - (node.propValue_vector.size() == 1)); - } else { - return true; - } + return (propValue_vector.equals(node.propValue_vector) || + propValue_vector.size() == 1 || + node.propValue_vector.size() == 1); } } @@ -154,4 +160,164 @@ public class PropertyNode { builder.append(propValue); return builder.toString(); } + + /** + * Encode this object into a string which can be decoded. + */ + public String encode() { + // PropertyNode#toString() is for reading, not for parsing in the future. + // We construct appropriate String here. + StringBuilder builder = new StringBuilder(); + if (propName.length() > 0) { + builder.append("propName:["); + builder.append(propName); + builder.append("],"); + } + int size = propGroupSet.size(); + if (size > 0) { + Set<String> set = propGroupSet; + builder.append("propGroup:["); + int i = 0; + for (String group : set) { + // We do not need to double quote groups. + // group = 1*(ALPHA / DIGIT / "-") + builder.append(group); + if (i < size - 1) { + builder.append(","); + } + i++; + } + builder.append("],"); + } + + if (paramMap.size() > 0 || paramMap_TYPE.size() > 0) { + ContentValues values = paramMap; + builder.append("paramMap:["); + size = paramMap.size(); + int i = 0; + for (Entry<String, Object> entry : values.valueSet()) { + // Assuming param-key does not contain NON-ASCII nor symbols. + // + // According to vCard 3.0: + // param-name = iana-token / x-name + builder.append(entry.getKey()); + + // param-value may contain any value including NON-ASCIIs. + // We use the following replacing rule. + // \ -> \\ + // , -> \, + // In String#replaceAll(), "\\\\" means a single backslash. + builder.append("="); + builder.append(entry.getValue().toString() + .replaceAll("\\\\", "\\\\\\\\") + .replaceAll(",", "\\\\,")); + if (i < size -1) { + builder.append(","); + } + i++; + } + + Set<String> set = paramMap_TYPE; + size = paramMap_TYPE.size(); + if (i > 0 && size > 0) { + builder.append(","); + } + i = 0; + for (String type : set) { + builder.append("TYPE="); + builder.append(type + .replaceAll("\\\\", "\\\\\\\\") + .replaceAll(",", "\\\\,")); + if (i < size - 1) { + builder.append(","); + } + i++; + } + builder.append("],"); + } + + size = propValue_vector.size(); + if (size > 0) { + builder.append("propValue:["); + List<String> list = propValue_vector; + for (int i = 0; i < size; i++) { + builder.append(list.get(i) + .replaceAll("\\\\", "\\\\\\\\") + .replaceAll(",", "\\\\,")); + if (i < size -1) { + builder.append(","); + } + } + builder.append("],"); + } + + return builder.toString(); + } + + public static PropertyNode decode(String encodedString) { + PropertyNode propertyNode = new PropertyNode(); + String trimed = encodedString.trim(); + if (trimed.length() == 0) { + return propertyNode; + } + String[] elems = trimed.split("],"); + + for (String elem : elems) { + int index = elem.indexOf('['); + String name = elem.substring(0, index - 1); + Pattern pattern = Pattern.compile("(?<!\\\\),"); + String[] values = pattern.split(elem.substring(index + 1), -1); + if (name.equals("propName")) { + propertyNode.propName = values[0]; + } else if (name.equals("propGroupSet")) { + for (String value : values) { + propertyNode.propGroupSet.add(value); + } + } else if (name.equals("paramMap")) { + ContentValues paramMap = propertyNode.paramMap; + Set<String> paramMap_TYPE = propertyNode.paramMap_TYPE; + for (String value : values) { + String[] tmp = value.split("=", 2); + String mapKey = tmp[0]; + // \, -> , + // \\ -> \ + // In String#replaceAll(), "\\\\" means a single backslash. + String mapValue = + tmp[1].replaceAll("\\\\,", ",").replaceAll("\\\\\\\\", "\\\\"); + if (mapKey.equalsIgnoreCase("TYPE")) { + paramMap_TYPE.add(mapValue); + } else { + paramMap.put(mapKey, mapValue); + } + } + } else if (name.equals("propValue")) { + StringBuilder builder = new StringBuilder(); + List<String> list = propertyNode.propValue_vector; + int length = values.length; + for (int i = 0; i < length; i++) { + String normValue = values[i] + .replaceAll("\\\\,", ",") + .replaceAll("\\\\\\\\", "\\\\"); + list.add(normValue); + builder.append(normValue); + if (i < length - 1) { + builder.append(";"); + } + } + propertyNode.propValue = builder.toString(); + } + } + + // At this time, QUOTED-PRINTABLE is already decoded to Java String. + // We just need to decode BASE64 String to binary. + String encoding = propertyNode.paramMap.getAsString("ENCODING"); + if (encoding != null && + (encoding.equalsIgnoreCase("BASE64") || + encoding.equalsIgnoreCase("B"))) { + propertyNode.propValue_bytes = + Base64.decodeBase64(propertyNode.propValue_vector.get(0).getBytes()); + } + + return propertyNode; + } } diff --git a/core/java/android/syncml/pim/VBuilderCollection.java b/core/java/android/syncml/pim/VBuilderCollection.java new file mode 100644 index 0000000..f09c1c4 --- /dev/null +++ b/core/java/android/syncml/pim/VBuilderCollection.java @@ -0,0 +1,100 @@ +/* + * 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.syncml.pim; + +import java.util.Collection; +import java.util.List; + +public class VBuilderCollection implements VBuilder { + + private final Collection<VBuilder> mVBuilderCollection; + + public VBuilderCollection(Collection<VBuilder> vBuilderCollection) { + mVBuilderCollection = vBuilderCollection; + } + + public Collection<VBuilder> getVBuilderCollection() { + return mVBuilderCollection; + } + + public void start() { + for (VBuilder builder : mVBuilderCollection) { + builder.start(); + } + } + + public void end() { + for (VBuilder builder : mVBuilderCollection) { + builder.end(); + } + } + + public void startRecord(String type) { + for (VBuilder builder : mVBuilderCollection) { + builder.startRecord(type); + } + } + + public void endRecord() { + for (VBuilder builder : mVBuilderCollection) { + builder.endRecord(); + } + } + + public void startProperty() { + for (VBuilder builder : mVBuilderCollection) { + builder.startProperty(); + } + } + + + public void endProperty() { + for (VBuilder builder : mVBuilderCollection) { + builder.endProperty(); + } + } + + public void propertyGroup(String group) { + for (VBuilder builder : mVBuilderCollection) { + builder.propertyGroup(group); + } + } + + public void propertyName(String name) { + for (VBuilder builder : mVBuilderCollection) { + builder.propertyName(name); + } + } + + public void propertyParamType(String type) { + for (VBuilder builder : mVBuilderCollection) { + builder.propertyParamType(type); + } + } + + public void propertyParamValue(String value) { + for (VBuilder builder : mVBuilderCollection) { + builder.propertyParamValue(value); + } + } + + public void propertyValues(List<String> values) { + for (VBuilder builder : mVBuilderCollection) { + builder.propertyValues(values); + } + } +} diff --git a/core/java/android/syncml/pim/VDataBuilder.java b/core/java/android/syncml/pim/VDataBuilder.java index 8c67cf5..f6e5b65 100644 --- a/core/java/android/syncml/pim/VDataBuilder.java +++ b/core/java/android/syncml/pim/VDataBuilder.java @@ -17,8 +17,10 @@ package android.syncml.pim; import android.content.ContentValues; +import android.util.CharsetUtils; import android.util.Log; +import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.net.QuotedPrintableCodec; @@ -26,9 +28,7 @@ import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.Vector; /** * Store the parse result to custom datastruct: VNode, PropertyNode @@ -38,7 +38,13 @@ import java.util.Vector; */ public class VDataBuilder implements VBuilder { static private String LOG_TAG = "VDATABuilder"; - + + /** + * If there's no other information available, this class uses this charset for encoding + * byte arrays. + */ + static public String DEFAULT_CHARSET = "UTF-8"; + /** type=VNode */ public List<VNode> vNodeList = new ArrayList<VNode>(); private int mNodeListPos = 0; @@ -47,34 +53,74 @@ public class VDataBuilder implements VBuilder { private String mCurrentParamType; /** - * Assumes that each String can be encoded into byte array using this encoding. + * The charset using which VParser parses the text. + */ + private String mSourceCharset; + + /** + * The charset with which byte array is encoded to String. */ - private String mCharset; + private String mTargetCharset; private boolean mStrictLineBreakParsing; public VDataBuilder() { - mCharset = "ISO-8859-1"; - mStrictLineBreakParsing = false; + this(VParser.DEFAULT_CHARSET, DEFAULT_CHARSET, false); } - public VDataBuilder(String encoding, boolean strictLineBreakParsing) { - mCharset = encoding; - mStrictLineBreakParsing = strictLineBreakParsing; + public VDataBuilder(String charset, boolean strictLineBreakParsing) { + this(null, charset, strictLineBreakParsing); } + /** + * @hide sourceCharset is temporal. + */ + public VDataBuilder(String sourceCharset, String targetCharset, + boolean strictLineBreakParsing) { + if (sourceCharset != null) { + mSourceCharset = sourceCharset; + } else { + mSourceCharset = VParser.DEFAULT_CHARSET; + } + if (targetCharset != null) { + mTargetCharset = targetCharset; + } else { + mTargetCharset = DEFAULT_CHARSET; + } + mStrictLineBreakParsing = strictLineBreakParsing; + } + public void start() { } public void end() { } + // Note: I guess that this code assumes the Record may nest like this: + // START:VPOS + // ... + // START:VPOS2 + // ... + // END:VPOS2 + // ... + // END:VPOS + // + // However the following code has a bug. + // When error occurs after calling startRecord(), the entry which is probably + // the cause of the error remains to be in vNodeList, while endRecord() is not called. + // + // I leave this code as is since I'm not familiar with vcalendar specification. + // But I believe we should refactor this code in the future. + // Until this, the last entry has to be removed when some error occurs. public void startRecord(String type) { + VNode vnode = new VNode(); vnode.parseStatus = 1; vnode.VName = type; + // I feel this should be done in endRecord(), but it cannot be done because of + // the reason above. vNodeList.add(vnode); - mNodeListPos = vNodeList.size()-1; + mNodeListPos = vNodeList.size() - 1; mCurrentVNode = vNodeList.get(mNodeListPos); } @@ -90,15 +136,14 @@ public class VDataBuilder implements VBuilder { } public void startProperty() { - // System.out.println("+ startProperty. "); + mCurrentPropNode = new PropertyNode(); } public void endProperty() { - // System.out.println("- endProperty. "); + mCurrentVNode.propList.add(mCurrentPropNode); } public void propertyName(String name) { - mCurrentPropNode = new PropertyNode(); mCurrentPropNode.propName = name; } @@ -122,139 +167,145 @@ public class VDataBuilder implements VBuilder { mCurrentParamType = null; } - private String encodeString(String originalString, String targetEncoding) { - Charset charset = Charset.forName(mCharset); + private String encodeString(String originalString, String targetCharset) { + if (mSourceCharset.equalsIgnoreCase(targetCharset)) { + return originalString; + } + Charset charset = Charset.forName(mSourceCharset); ByteBuffer byteBuffer = charset.encode(originalString); // byteBuffer.array() "may" return byte array which is larger than // byteBuffer.remaining(). Here, we keep on the safe side. byte[] bytes = new byte[byteBuffer.remaining()]; byteBuffer.get(bytes); try { - return new String(bytes, targetEncoding); + return new String(bytes, targetCharset); } catch (UnsupportedEncodingException e) { - return null; + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return new String(bytes); } } - public void propertyValues(List<String> values) { - ContentValues paramMap = mCurrentPropNode.paramMap; - - String charsetString = paramMap.getAsString("CHARSET"); - - boolean setupParamValues = false; - //decode value string to propValue_bytes - if (paramMap.containsKey("ENCODING")) { - String encoding = paramMap.getAsString("ENCODING"); - if (encoding.equalsIgnoreCase("BASE64") || - encoding.equalsIgnoreCase("B")) { - if (values.size() > 1) { - Log.e(LOG_TAG, - ("BASE64 encoding is used while " + - "there are multiple values (" + values.size())); - } + private String handleOneValue(String value, String targetCharset, String encoding) { + if (encoding != null) { + if (encoding.equals("BASE64") || encoding.equals("B")) { + // Assume BASE64 is used only when the number of values is 1. mCurrentPropNode.propValue_bytes = - Base64.decodeBase64(values.get(0). - replaceAll(" ","").replaceAll("\t",""). - replaceAll("\r\n",""). - getBytes()); - } - - if(encoding.equalsIgnoreCase("QUOTED-PRINTABLE")){ - // if CHARSET is defined, we translate each String into the Charset. - List<String> tmpValues = new ArrayList<String>(); - Vector<byte[]> byteVector = new Vector<byte[]>(); - int size = 0; - try{ - for (String value : values) { - String quotedPrintable = value - .replaceAll("= ", " ").replaceAll("=\t", "\t"); - String[] lines; - if (mStrictLineBreakParsing) { - lines = quotedPrintable.split("\r\n"); - } else { - lines = quotedPrintable - .replace("\r\n", "\n").replace("\r", "\n").split("\n"); - } - StringBuilder builder = new StringBuilder(); - for (String line : lines) { - if (line.endsWith("=")) { - line = line.substring(0, line.length() - 1); - } - builder.append(line); - } - byte[] bytes = QuotedPrintableCodec.decodeQuotedPrintable( - builder.toString().getBytes()); - if (charsetString != null) { - try { - tmpValues.add(new String(bytes, charsetString)); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + charsetString); - tmpValues.add(new String(bytes)); + Base64.decodeBase64(value.getBytes()); + return value; + } else if (encoding.equals("QUOTED-PRINTABLE")) { + String quotedPrintable = value + .replaceAll("= ", " ").replaceAll("=\t", "\t"); + String[] lines; + if (mStrictLineBreakParsing) { + lines = quotedPrintable.split("\r\n"); + } else { + StringBuilder builder = new StringBuilder(); + int length = quotedPrintable.length(); + ArrayList<String> list = new ArrayList<String>(); + for (int i = 0; i < length; i++) { + char ch = quotedPrintable.charAt(i); + if (ch == '\n') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else if (ch == '\r') { + list.add(builder.toString()); + builder = new StringBuilder(); + if (i < length - 1) { + char nextCh = quotedPrintable.charAt(i + 1); + if (nextCh == '\n') { + i++; + } } } else { - tmpValues.add(new String(bytes)); - } - byteVector.add(bytes); - size += bytes.length; - } // for (String value : values) { - mCurrentPropNode.propValue_vector = tmpValues; - mCurrentPropNode.propValue = listToString(tmpValues); - - mCurrentPropNode.propValue_bytes = new byte[size]; - - { - byte[] tmpBytes = mCurrentPropNode.propValue_bytes; - int index = 0; - for (byte[] bytes : byteVector) { - int length = bytes.length; - for (int i = 0; i < length; i++, index++) { - tmpBytes[index] = bytes[i]; - } + builder.append(ch); } } - setupParamValues = true; - } catch(Exception e) { - Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e); + String finalLine = builder.toString(); + if (finalLine.length() > 0) { + list.add(finalLine); + } + lines = list.toArray(new String[0]); } - } // QUOTED-PRINTABLE - } // ENCODING - - if (!setupParamValues) { - // if CHARSET is defined, we translate each String into the Charset. - if (charsetString != null) { - List<String> tmpValues = new ArrayList<String>(); - for (String value : values) { - String result = encodeString(value, charsetString); - if (result != null) { - tmpValues.add(result); - } else { - Log.e(LOG_TAG, "Failed to encode: charset=" + charsetString); - tmpValues.add(value); + StringBuilder builder = new StringBuilder(); + for (String line : lines) { + if (line.endsWith("=")) { + line = line.substring(0, line.length() - 1); } + builder.append(line); + } + byte[] bytes; + try { + bytes = builder.toString().getBytes(mSourceCharset); + } catch (UnsupportedEncodingException e1) { + Log.e(LOG_TAG, "Failed to encode: charset=" + mSourceCharset); + bytes = builder.toString().getBytes(); + } + + try { + bytes = QuotedPrintableCodec.decodeQuotedPrintable(bytes); + } catch (DecoderException e) { + Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e); + return ""; + } + + try { + return new String(bytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return new String(bytes); } - values = tmpValues; } - - mCurrentPropNode.propValue_vector = values; - mCurrentPropNode.propValue = listToString(values); + // Unknown encoding. Fall back to default. } - mCurrentVNode.propList.add(mCurrentPropNode); + return encodeString(value, targetCharset); } - - private String listToString(Collection<String> list){ - StringBuilder typeListB = new StringBuilder(); - for (String type : list) { - typeListB.append(type).append(";"); + + public void propertyValues(List<String> values) { + if (values == null || values.size() == 0) { + mCurrentPropNode.propValue_bytes = null; + mCurrentPropNode.propValue_vector.clear(); + mCurrentPropNode.propValue_vector.add(""); + mCurrentPropNode.propValue = ""; + return; + } + + ContentValues paramMap = mCurrentPropNode.paramMap; + + String targetCharset = CharsetUtils.nameForDefaultVendor(paramMap.getAsString("CHARSET")); + String encoding = paramMap.getAsString("ENCODING"); + + if (targetCharset == null || targetCharset.length() == 0) { + targetCharset = mTargetCharset; + } + + for (String value : values) { + mCurrentPropNode.propValue_vector.add( + handleOneValue(value, targetCharset, encoding)); } - int len = typeListB.length(); - if (len > 0 && typeListB.charAt(len - 1) == ';') { - return typeListB.substring(0, len - 1); + + mCurrentPropNode.propValue = listToString(mCurrentPropNode.propValue_vector); + } + + private String listToString(List<String> list){ + int size = list.size(); + if (size > 1) { + StringBuilder typeListB = new StringBuilder(); + for (String type : list) { + typeListB.append(type).append(";"); + } + int len = typeListB.length(); + if (len > 0 && typeListB.charAt(len - 1) == ';') { + return typeListB.substring(0, len - 1); + } + return typeListB.toString(); + } else if (size == 1) { + return list.get(0); + } else { + return ""; } - return typeListB.toString(); } public String getResult(){ return null; } } - diff --git a/core/java/android/syncml/pim/VParser.java b/core/java/android/syncml/pim/VParser.java index df93f38..57c5f7a 100644 --- a/core/java/android/syncml/pim/VParser.java +++ b/core/java/android/syncml/pim/VParser.java @@ -26,6 +26,9 @@ import java.io.UnsupportedEncodingException; * */ abstract public class VParser { + // Assume that "iso-8859-1" is able to map "all" 8bit characters to some unicode and + // decode the unicode to the original charset. If not, this setting will cause some bug. + public static String DEFAULT_CHARSET = "iso-8859-1"; /** * The buffer used to store input stream @@ -96,6 +99,20 @@ abstract public class VParser { } /** + * Parse the given stream with the default encoding. + * + * @param is + * The source to parse. + * @param builder + * The v builder which used to construct data. + * @return Return true for success, otherwise false. + * @throws IOException + */ + public boolean parse(InputStream is, VBuilder builder) throws IOException { + return parse(is, DEFAULT_CHARSET, builder); + } + + /** * Copy the content of input stream and filter the "folding" */ protected void setInputStream(InputStream is, String encoding) diff --git a/core/java/android/syncml/pim/vcard/ContactStruct.java b/core/java/android/syncml/pim/vcard/ContactStruct.java index 8d9b7fa..ecd719d 100644 --- a/core/java/android/syncml/pim/vcard/ContactStruct.java +++ b/core/java/android/syncml/pim/vcard/ContactStruct.java @@ -16,45 +16,103 @@ package android.syncml.pim.vcard; -import java.util.List; +import android.content.AbstractSyncableContentProvider; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.net.Uri; +import android.provider.Contacts; +import android.provider.Contacts.ContactMethods; +import android.provider.Contacts.Extensions; +import android.provider.Contacts.GroupMembership; +import android.provider.Contacts.Organizations; +import android.provider.Contacts.People; +import android.provider.Contacts.Phones; +import android.provider.Contacts.Photos; +import android.syncml.pim.PropertyNode; +import android.syncml.pim.VNode; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; + import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; /** - * The parameter class of VCardCreator. + * The parameter class of VCardComposer. * This class standy by the person-contact in * Android system, we must use this class instance as parameter to transmit to - * VCardCreator so that create vCard string. + * VCardComposer so that create vCard string. */ // TODO: rename the class name, next step public class ContactStruct { - public String company; + private static final String LOG_TAG = "ContactStruct"; + + // Note: phonetic name probably should be "LAST FIRST MIDDLE" for European languages, and + // space should be added between each element while it should not be in Japanese. + // But unfortunately, we currently do not have the data and are not sure whether we should + // support European version of name ordering. + // + // TODO: Implement the logic described above if we really need European version of + // phonetic name handling. Also, adding the appropriate test case of vCard would be + // highly appreciated. + public static final int NAME_ORDER_TYPE_ENGLISH = 0; + public static final int NAME_ORDER_TYPE_JAPANESE = 1; + /** MUST exist */ public String name; + public String phoneticName; /** maybe folding */ - public String notes; + public List<String> notes = new ArrayList<String>(); /** maybe folding */ public String title; /** binary bytes of pic. */ public byte[] photoBytes; - /** mime_type col of images table */ + /** The type of Photo (e.g. JPEG, BMP, etc.) */ public String photoType; /** Only for GET. Use addPhoneList() to PUT. */ public List<PhoneData> phoneList; /** Only for GET. Use addContactmethodList() to PUT. */ public List<ContactMethod> contactmethodList; + /** Only for GET. Use addOrgList() to PUT. */ + public List<OrganizationData> organizationList; + /** Only for GET. Use addExtension() to PUT */ + public Map<String, List<String>> extensionMap; - public static class PhoneData{ + // Use organizationList instead when handling ORG. + @Deprecated + public String company; + + public static class PhoneData { + public int type; /** maybe folding */ public String data; - public String type; public String label; + public boolean isPrimary; } - public static class ContactMethod{ - public String kind; - public String type; + public static class ContactMethod { + // Contacts.KIND_EMAIL, Contacts.KIND_POSTAL + public int kind; + // e.g. Contacts.ContactMethods.TYPE_HOME, Contacts.PhoneColumns.TYPE_HOME + // If type == Contacts.PhoneColumns.TYPE_CUSTOM, label is used. + public int type; public String data; + // Used only when TYPE is TYPE_CUSTOM. public String label; + public boolean isPrimary; + } + + public static class OrganizationData { + public int type; + public String companyName; + public String positionName; + public boolean isPrimary; } /** @@ -63,29 +121,858 @@ public class ContactStruct { * @param type type col of content://contacts/phones * @param label lable col of content://contacts/phones */ - public void addPhone(String data, String type, String label){ - if(phoneList == null) + public void addPhone(int type, String data, String label, boolean isPrimary){ + if (phoneList == null) { phoneList = new ArrayList<PhoneData>(); - PhoneData st = new PhoneData(); - st.data = data; - st.type = type; - st.label = label; - phoneList.add(st); + } + PhoneData phoneData = new PhoneData(); + phoneData.type = type; + + StringBuilder builder = new StringBuilder(); + String trimed = data.trim(); + int length = trimed.length(); + for (int i = 0; i < length; i++) { + char ch = trimed.charAt(i); + if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { + builder.append(ch); + } + } + phoneData.data = PhoneNumberUtils.formatNumber(builder.toString()); + phoneData.label = label; + phoneData.isPrimary = isPrimary; + phoneList.add(phoneData); } + /** * Add a contactmethod info to contactmethodList. - * @param data contact data + * @param kind integer value defined in Contacts.java + * (e.g. Contacts.KIND_EMAIL) * @param type type col of content://contacts/contact_methods + * @param data contact data + * @param label extra string used only when kind is Contacts.KIND_CUSTOM. */ - public void addContactmethod(String kind, String data, String type, - String label){ - if(contactmethodList == null) + public void addContactmethod(int kind, int type, String data, + String label, boolean isPrimary){ + if (contactmethodList == null) { contactmethodList = new ArrayList<ContactMethod>(); - ContactMethod st = new ContactMethod(); - st.kind = kind; - st.data = data; - st.type = type; - st.label = label; - contactmethodList.add(st); + } + ContactMethod contactMethod = new ContactMethod(); + contactMethod.kind = kind; + contactMethod.type = type; + contactMethod.data = data; + contactMethod.label = label; + contactMethod.isPrimary = isPrimary; + contactmethodList.add(contactMethod); + } + + /** + * Add a Organization info to organizationList. + */ + public void addOrganization(int type, String companyName, String positionName, + boolean isPrimary) { + if (organizationList == null) { + organizationList = new ArrayList<OrganizationData>(); + } + OrganizationData organizationData = new OrganizationData(); + organizationData.type = type; + organizationData.companyName = companyName; + organizationData.positionName = positionName; + organizationData.isPrimary = isPrimary; + organizationList.add(organizationData); + } + + /** + * Set "position" value to the appropriate data. If there's more than one + * OrganizationData objects, the value is set to the last one. If there's no + * OrganizationData object, a new OrganizationData is created, whose company name is + * empty. + * + * TODO: incomplete logic. fix this: + * + * e.g. This assumes ORG comes earlier, but TITLE may come earlier like this, though we do not + * know how to handle it in general cases... + * ---- + * TITLE:Software Engineer + * ORG:Google + * ---- + */ + public void setPosition(String positionValue) { + if (organizationList == null) { + organizationList = new ArrayList<OrganizationData>(); + } + int size = organizationList.size(); + if (size == 0) { + addOrganization(Contacts.OrganizationColumns.TYPE_OTHER, "", null, false); + size = 1; + } + OrganizationData lastData = organizationList.get(size - 1); + lastData.positionName = positionValue; + } + + public void addExtension(PropertyNode propertyNode) { + if (propertyNode.propValue.length() == 0) { + return; + } + // Now store the string into extensionMap. + List<String> list; + String name = propertyNode.propName; + if (extensionMap == null) { + extensionMap = new HashMap<String, List<String>>(); + } + if (!extensionMap.containsKey(name)){ + list = new ArrayList<String>(); + extensionMap.put(name, list); + } else { + list = extensionMap.get(name); + } + + list.add(propertyNode.encode()); + } + + private static String getNameFromNProperty(List<String> elems, int nameOrderType) { + // Family, Given, Middle, Prefix, Suffix. (1 - 5) + int size = elems.size(); + if (size > 1) { + StringBuilder builder = new StringBuilder(); + boolean builderIsEmpty = true; + // Prefix + if (size > 3 && elems.get(3).length() > 0) { + builder.append(elems.get(3)); + builderIsEmpty = false; + } + String first, second; + if (nameOrderType == NAME_ORDER_TYPE_JAPANESE) { + first = elems.get(0); + second = elems.get(1); + } else { + first = elems.get(1); + second = elems.get(0); + } + if (first.length() > 0) { + if (!builderIsEmpty) { + builder.append(' '); + } + builder.append(first); + builderIsEmpty = false; + } + // Middle name + if (size > 2 && elems.get(2).length() > 0) { + if (!builderIsEmpty) { + builder.append(' '); + } + builder.append(elems.get(2)); + builderIsEmpty = false; + } + if (second.length() > 0) { + if (!builderIsEmpty) { + builder.append(' '); + } + builder.append(second); + builderIsEmpty = false; + } + // Suffix + if (size > 4 && elems.get(4).length() > 0) { + if (!builderIsEmpty) { + builder.append(' '); + } + builder.append(elems.get(4)); + builderIsEmpty = false; + } + return builder.toString(); + } else if (size == 1) { + return elems.get(0); + } else { + return ""; + } + } + + public static ContactStruct constructContactFromVNode(VNode node, + int nameOrderType) { + if (!node.VName.equals("VCARD")) { + // Impossible in current implementation. Just for safety. + Log.e(LOG_TAG, "Non VCARD data is inserted."); + return null; + } + + // For name, there are three fields in vCard: FN, N, NAME. + // We prefer FN, which is a required field in vCard 3.0 , but not in vCard 2.1. + // Next, we prefer NAME, which is defined only in vCard 3.0. + // Finally, we use N, which is a little difficult to parse. + String fullName = null; + String nameFromNProperty = null; + + // Some vCard has "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", and + // "X-PHONETIC-LAST-NAME" + String xPhoneticFirstName = null; + String xPhoneticMiddleName = null; + String xPhoneticLastName = null; + + ContactStruct contact = new ContactStruct(); + + // Each Column of four properties has ISPRIMARY field + // (See android.provider.Contacts) + // If false even after the following loop, we choose the first + // entry as a "primary" entry. + boolean prefIsSetAddress = false; + boolean prefIsSetPhone = false; + boolean prefIsSetEmail = false; + boolean prefIsSetOrganization = false; + + for (PropertyNode propertyNode: node.propList) { + String name = propertyNode.propName; + + if (TextUtils.isEmpty(propertyNode.propValue)) { + continue; + } + + if (name.equals("VERSION")) { + // vCard version. Ignore this. + } else if (name.equals("FN")) { + fullName = propertyNode.propValue; + } else if (name.equals("NAME") && fullName == null) { + // Only in vCard 3.0. Use this if FN does not exist. + // Though, note that vCard 3.0 requires FN. + fullName = propertyNode.propValue; + } else if (name.equals("N")) { + nameFromNProperty = getNameFromNProperty(propertyNode.propValue_vector, + nameOrderType); + } else if (name.equals("SORT-STRING")) { + contact.phoneticName = propertyNode.propValue; + } else if (name.equals("SOUND")) { + if (propertyNode.paramMap_TYPE.contains("X-IRMC-N") && + contact.phoneticName == null) { + // Some Japanese mobile phones use this field for phonetic name, + // since vCard 2.1 does not have "SORT-STRING" type. + // Also, in some cases, the field has some ';' in it. + // We remove them. + StringBuilder builder = new StringBuilder(); + String value = propertyNode.propValue; + int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (ch != ';') { + builder.append(ch); + } + } + contact.phoneticName = builder.toString(); + } else { + contact.addExtension(propertyNode); + } + } else if (name.equals("ADR")) { + List<String> values = propertyNode.propValue_vector; + boolean valuesAreAllEmpty = true; + for (String value : values) { + if (value.length() > 0) { + valuesAreAllEmpty = false; + break; + } + } + if (valuesAreAllEmpty) { + continue; + } + + int kind = Contacts.KIND_POSTAL; + int type = -1; + String label = ""; + boolean isPrimary = false; + for (String typeString : propertyNode.paramMap_TYPE) { + if (typeString.equals("PREF") && !prefIsSetAddress) { + // Only first "PREF" is considered. + prefIsSetAddress = true; + isPrimary = true; + } else if (typeString.equalsIgnoreCase("HOME")) { + type = Contacts.ContactMethodsColumns.TYPE_HOME; + label = ""; + } else if (typeString.equalsIgnoreCase("WORK") || + typeString.equalsIgnoreCase("COMPANY")) { + // "COMPANY" seems emitted by Windows Mobile, which is not + // specifically supported by vCard 2.1. We assume this is same + // as "WORK". + type = Contacts.ContactMethodsColumns.TYPE_WORK; + label = ""; + } else if (typeString.equalsIgnoreCase("POSTAL")) { + kind = Contacts.KIND_POSTAL; + } else if (typeString.equalsIgnoreCase("PARCEL") || + typeString.equalsIgnoreCase("DOM") || + typeString.equalsIgnoreCase("INTL")) { + // We do not have a kind or type matching these. + // TODO: fix this. We may need to split entries into two. + // (e.g. entries for KIND_POSTAL and KIND_PERCEL) + } else if (typeString.toUpperCase().startsWith("X-") && + type < 0) { + type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; + label = typeString.substring(2); + } else if (type < 0) { + // vCard 3.0 allows iana-token. Also some vCard 2.1 exporters + // emit non-standard types. We do not handle their values now. + type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; + label = typeString; + } + } + // We use "HOME" as default + if (type < 0) { + type = Contacts.ContactMethodsColumns.TYPE_HOME; + } + + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name + String address; + List<String> list = propertyNode.propValue_vector; + int size = list.size(); + if (size > 1) { + StringBuilder builder = new StringBuilder(); + boolean builderIsEmpty = true; + if (Locale.getDefault().getCountry().equals(Locale.JAPAN.getCountry())) { + // In Japan, the order is reversed. + for (int i = size - 1; i >= 0; i--) { + String addressPart = list.get(i); + if (addressPart.length() > 0) { + if (!builderIsEmpty) { + builder.append(' '); + } + builder.append(addressPart); + builderIsEmpty = false; + } + } + } else { + for (int i = 0; i < size; i++) { + String addressPart = list.get(i); + if (addressPart.length() > 0) { + if (!builderIsEmpty) { + builder.append(' '); + } + builder.append(addressPart); + builderIsEmpty = false; + } + } + } + address = builder.toString().trim(); + } else { + address = propertyNode.propValue; + } + contact.addContactmethod(kind, type, address, label, isPrimary); + } else if (name.equals("ORG")) { + // vCard specification does not specify other types. + int type = Contacts.OrganizationColumns.TYPE_WORK; + boolean isPrimary = false; + + for (String typeString : propertyNode.paramMap_TYPE) { + if (typeString.equals("PREF") && !prefIsSetOrganization) { + // vCard specification officially does not have PREF in ORG. + // This is just for safety. + prefIsSetOrganization = true; + isPrimary = true; + } + // XXX: Should we cope with X- words? + } + + List<String> list = propertyNode.propValue_vector; + int size = list.size(); + StringBuilder builder = new StringBuilder(); + for (Iterator<String> iter = list.iterator(); iter.hasNext();) { + builder.append(iter.next()); + if (iter.hasNext()) { + builder.append(' '); + } + } + + contact.addOrganization(type, builder.toString(), "", isPrimary); + } else if (name.equals("TITLE")) { + contact.setPosition(propertyNode.propValue); + } else if (name.equals("ROLE")) { + contact.setPosition(propertyNode.propValue); + } else if (name.equals("PHOTO")) { + // We prefer PHOTO to LOGO. + String valueType = propertyNode.paramMap.getAsString("VALUE"); + if (valueType != null && valueType.equals("URL")) { + // TODO: do something. + } else { + // Assume PHOTO is stored in BASE64. In that case, + // data is already stored in propValue_bytes in binary form. + // It should be automatically done by VBuilder (VDataBuilder/VCardDatabuilder) + contact.photoBytes = propertyNode.propValue_bytes; + String type = propertyNode.paramMap.getAsString("TYPE"); + if (type != null) { + contact.photoType = type; + } + } + } else if (name.equals("LOGO")) { + // When PHOTO is not available this is not URL, + // we use this instead of PHOTO. + String valueType = propertyNode.paramMap.getAsString("VALUE"); + if (valueType != null && valueType.equals("URL")) { + // TODO: do something. + } else if (contact.photoBytes == null) { + contact.photoBytes = propertyNode.propValue_bytes; + String type = propertyNode.paramMap.getAsString("TYPE"); + if (type != null) { + contact.photoType = type; + } + } + } else if (name.equals("EMAIL")) { + int type = -1; + String label = null; + boolean isPrimary = false; + for (String typeString : propertyNode.paramMap_TYPE) { + if (typeString.equals("PREF") && !prefIsSetEmail) { + // Only first "PREF" is considered. + prefIsSetEmail = true; + isPrimary = true; + } else if (typeString.equalsIgnoreCase("HOME")) { + type = Contacts.ContactMethodsColumns.TYPE_HOME; + } else if (typeString.equalsIgnoreCase("WORK")) { + type = Contacts.ContactMethodsColumns.TYPE_WORK; + } else if (typeString.equalsIgnoreCase("CELL")) { + // We do not have Contacts.ContactMethodsColumns.TYPE_MOBILE yet. + type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; + label = Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME; + } else if (typeString.toUpperCase().startsWith("X-") && + type < 0) { + type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; + label = typeString.substring(2); + } else if (type < 0) { + // vCard 3.0 allows iana-token. + // We may have INTERNET (specified in vCard spec), + // SCHOOL, etc. + type = Contacts.ContactMethodsColumns.TYPE_CUSTOM; + label = typeString; + } + } + // We use "OTHER" as default. + if (type < 0) { + type = Contacts.ContactMethodsColumns.TYPE_OTHER; + } + contact.addContactmethod(Contacts.KIND_EMAIL, + type, propertyNode.propValue,label, isPrimary); + } else if (name.equals("TEL")) { + int type = -1; + String label = null; + boolean isPrimary = false; + boolean isFax = false; + for (String typeString : propertyNode.paramMap_TYPE) { + if (typeString.equals("PREF") && !prefIsSetPhone) { + // Only first "PREF" is considered. + prefIsSetPhone = true; + isPrimary = true; + } else if (typeString.equalsIgnoreCase("HOME")) { + type = Contacts.PhonesColumns.TYPE_HOME; + } else if (typeString.equalsIgnoreCase("WORK")) { + type = Contacts.PhonesColumns.TYPE_WORK; + } else if (typeString.equalsIgnoreCase("CELL")) { + type = Contacts.PhonesColumns.TYPE_MOBILE; + } else if (typeString.equalsIgnoreCase("PAGER")) { + type = Contacts.PhonesColumns.TYPE_PAGER; + } else if (typeString.equalsIgnoreCase("FAX")) { + isFax = true; + } else if (typeString.equalsIgnoreCase("VOICE") || + typeString.equalsIgnoreCase("MSG")) { + // Defined in vCard 3.0. Ignore these because they + // conflict with "HOME", "WORK", etc. + // XXX: do something? + } else if (typeString.toUpperCase().startsWith("X-") && + type < 0) { + type = Contacts.PhonesColumns.TYPE_CUSTOM; + label = typeString.substring(2); + } else if (type < 0){ + // We may have MODEM, CAR, ISDN, etc... + type = Contacts.PhonesColumns.TYPE_CUSTOM; + label = typeString; + } + } + // We use "HOME" as default + if (type < 0) { + type = Contacts.PhonesColumns.TYPE_HOME; + } + if (isFax) { + if (type == Contacts.PhonesColumns.TYPE_HOME) { + type = Contacts.PhonesColumns.TYPE_FAX_HOME; + } else if (type == Contacts.PhonesColumns.TYPE_WORK) { + type = Contacts.PhonesColumns.TYPE_FAX_WORK; + } + } + + contact.addPhone(type, propertyNode.propValue, label, isPrimary); + } else if (name.equals("NOTE")) { + contact.notes.add(propertyNode.propValue); + } else if (name.equals("BDAY")) { + contact.addExtension(propertyNode); + } else if (name.equals("URL")) { + contact.addExtension(propertyNode); + } else if (name.equals("REV")) { + // Revision of this VCard entry. I think we can ignore this. + contact.addExtension(propertyNode); + } else if (name.equals("UID")) { + contact.addExtension(propertyNode); + } else if (name.equals("KEY")) { + // Type is X509 or PGP? I don't know how to handle this... + contact.addExtension(propertyNode); + } else if (name.equals("MAILER")) { + contact.addExtension(propertyNode); + } else if (name.equals("TZ")) { + contact.addExtension(propertyNode); + } else if (name.equals("GEO")) { + contact.addExtension(propertyNode); + } else if (name.equals("NICKNAME")) { + // vCard 3.0 only. + contact.addExtension(propertyNode); + } else if (name.equals("CLASS")) { + // vCard 3.0 only. + // e.g. CLASS:CONFIDENTIAL + contact.addExtension(propertyNode); + } else if (name.equals("PROFILE")) { + // VCard 3.0 only. Must be "VCARD". I think we can ignore this. + contact.addExtension(propertyNode); + } else if (name.equals("CATEGORIES")) { + // VCard 3.0 only. + // e.g. CATEGORIES:INTERNET,IETF,INDUSTRY,INFORMATION TECHNOLOGY + contact.addExtension(propertyNode); + } else if (name.equals("SOURCE")) { + // VCard 3.0 only. + contact.addExtension(propertyNode); + } else if (name.equals("PRODID")) { + // VCard 3.0 only. + // To specify the identifier for the product that created + // the vCard object. + contact.addExtension(propertyNode); + } else if (name.equals("X-PHONETIC-FIRST-NAME")) { + xPhoneticFirstName = propertyNode.propValue; + } else if (name.equals("X-PHONETIC-MIDDLE-NAME")) { + xPhoneticMiddleName = propertyNode.propValue; + } else if (name.equals("X-PHONETIC-LAST-NAME")) { + xPhoneticLastName = propertyNode.propValue; + } else { + // Unknown X- words and IANA token. + contact.addExtension(propertyNode); + } + } + + if (fullName != null) { + contact.name = fullName; + } else if(nameFromNProperty != null) { + contact.name = nameFromNProperty; + } else { + contact.name = ""; + } + + if (contact.phoneticName == null && + (xPhoneticFirstName != null || xPhoneticMiddleName != null || + xPhoneticLastName != null)) { + // Note: In Europe, this order should be "LAST FIRST MIDDLE". See the comment around + // NAME_ORDER_TYPE_* for more detail. + String first; + String second; + if (nameOrderType == NAME_ORDER_TYPE_JAPANESE) { + first = xPhoneticLastName; + second = xPhoneticFirstName; + } else { + first = xPhoneticFirstName; + second = xPhoneticLastName; + } + StringBuilder builder = new StringBuilder(); + if (first != null) { + builder.append(first); + } + if (xPhoneticMiddleName != null) { + builder.append(xPhoneticMiddleName); + } + if (second != null) { + builder.append(second); + } + contact.phoneticName = builder.toString(); + } + + // Remove unnecessary white spaces. + // It is found that some mobile phone emits phonetic name with just one white space + // when a user does not specify one. + // This logic is effective toward such kind of weird data. + if (contact.phoneticName != null) { + contact.phoneticName = contact.phoneticName.trim(); + } + + // If there is no "PREF", we choose the first entries as primary. + if (!prefIsSetPhone && + contact.phoneList != null && + contact.phoneList.size() > 0) { + contact.phoneList.get(0).isPrimary = true; + } + + if (!prefIsSetAddress && contact.contactmethodList != null) { + for (ContactMethod contactMethod : contact.contactmethodList) { + if (contactMethod.kind == Contacts.KIND_POSTAL) { + contactMethod.isPrimary = true; + break; + } + } + } + if (!prefIsSetEmail && contact.contactmethodList != null) { + for (ContactMethod contactMethod : contact.contactmethodList) { + if (contactMethod.kind == Contacts.KIND_EMAIL) { + contactMethod.isPrimary = true; + break; + } + } + } + if (!prefIsSetOrganization && + contact.organizationList != null && + contact.organizationList.size() > 0) { + contact.organizationList.get(0).isPrimary = true; + } + + return contact; + } + + public String displayString() { + if (name.length() > 0) { + return name; + } + if (contactmethodList != null && contactmethodList.size() > 0) { + for (ContactMethod contactMethod : contactmethodList) { + if (contactMethod.kind == Contacts.KIND_EMAIL && contactMethod.isPrimary) { + return contactMethod.data; + } + } + } + if (phoneList != null && phoneList.size() > 0) { + for (PhoneData phoneData : phoneList) { + if (phoneData.isPrimary) { + return phoneData.data; + } + } + } + return ""; + } + + private void pushIntoContentProviderOrResolver(Object contentSomething, + long myContactsGroupId) { + ContentResolver resolver = null; + AbstractSyncableContentProvider provider = null; + if (contentSomething instanceof ContentResolver) { + resolver = (ContentResolver)contentSomething; + } else if (contentSomething instanceof AbstractSyncableContentProvider) { + provider = (AbstractSyncableContentProvider)contentSomething; + } else { + Log.e(LOG_TAG, "Unsupported object came."); + return; + } + + ContentValues contentValues = new ContentValues(); + contentValues.put(People.NAME, name); + contentValues.put(People.PHONETIC_NAME, phoneticName); + + if (notes.size() > 1) { + StringBuilder builder = new StringBuilder(); + for (String note : notes) { + builder.append(note); + builder.append("\n"); + } + contentValues.put(People.NOTES, builder.toString()); + } else if (notes.size() == 1){ + contentValues.put(People.NOTES, notes.get(0)); + } + + Uri personUri; + long personId = 0; + if (resolver != null) { + personUri = Contacts.People.createPersonInMyContactsGroup( + resolver, contentValues); + if (personUri != null) { + personId = ContentUris.parseId(personUri); + } + } else { + personUri = provider.nonTransactionalInsert(People.CONTENT_URI, contentValues); + if (personUri != null) { + personId = ContentUris.parseId(personUri); + ContentValues values = new ContentValues(); + values.put(GroupMembership.PERSON_ID, personId); + values.put(GroupMembership.GROUP_ID, myContactsGroupId); + Uri resultUri = provider.nonTransactionalInsert( + GroupMembership.CONTENT_URI, values); + if (resultUri == null) { + Log.e(LOG_TAG, "Faild to insert the person to MyContact."); + provider.nonTransactionalDelete(personUri, null, null); + personUri = null; + } + } + } + + if (personUri == null) { + Log.e(LOG_TAG, "Failed to create the contact."); + return; + } + + if (photoBytes != null) { + if (resolver != null) { + People.setPhotoData(resolver, personUri, photoBytes); + } else { + Uri photoUri = Uri.withAppendedPath(personUri, Contacts.Photos.CONTENT_DIRECTORY); + ContentValues values = new ContentValues(); + values.put(Photos.DATA, photoBytes); + provider.update(photoUri, values, null, null); + } + } + + long primaryPhoneId = -1; + if (phoneList != null && phoneList.size() > 0) { + for (PhoneData phoneData : phoneList) { + ContentValues values = new ContentValues(); + values.put(Contacts.PhonesColumns.TYPE, phoneData.type); + if (phoneData.type == Contacts.PhonesColumns.TYPE_CUSTOM) { + values.put(Contacts.PhonesColumns.LABEL, phoneData.label); + } + // Already formatted. + values.put(Contacts.PhonesColumns.NUMBER, phoneData.data); + + // Not sure about Contacts.PhonesColumns.NUMBER_KEY ... + values.put(Contacts.PhonesColumns.ISPRIMARY, 1); + values.put(Contacts.Phones.PERSON_ID, personId); + Uri phoneUri; + if (resolver != null) { + phoneUri = resolver.insert(Phones.CONTENT_URI, values); + } else { + phoneUri = provider.nonTransactionalInsert(Phones.CONTENT_URI, values); + } + if (phoneData.isPrimary) { + primaryPhoneId = Long.parseLong(phoneUri.getLastPathSegment()); + } + } + } + + long primaryOrganizationId = -1; + if (organizationList != null && organizationList.size() > 0) { + for (OrganizationData organizationData : organizationList) { + ContentValues values = new ContentValues(); + // Currently, we do not use TYPE_CUSTOM. + values.put(Contacts.OrganizationColumns.TYPE, + organizationData.type); + values.put(Contacts.OrganizationColumns.COMPANY, + organizationData.companyName); + values.put(Contacts.OrganizationColumns.TITLE, + organizationData.positionName); + values.put(Contacts.OrganizationColumns.ISPRIMARY, 1); + values.put(Contacts.OrganizationColumns.PERSON_ID, personId); + + Uri organizationUri; + if (resolver != null) { + organizationUri = resolver.insert(Organizations.CONTENT_URI, values); + } else { + organizationUri = provider.nonTransactionalInsert( + Organizations.CONTENT_URI, values); + } + if (organizationData.isPrimary) { + primaryOrganizationId = Long.parseLong(organizationUri.getLastPathSegment()); + } + } + } + + long primaryEmailId = -1; + if (contactmethodList != null && contactmethodList.size() > 0) { + for (ContactMethod contactMethod : contactmethodList) { + ContentValues values = new ContentValues(); + values.put(Contacts.ContactMethodsColumns.KIND, contactMethod.kind); + values.put(Contacts.ContactMethodsColumns.TYPE, contactMethod.type); + if (contactMethod.type == Contacts.ContactMethodsColumns.TYPE_CUSTOM) { + values.put(Contacts.ContactMethodsColumns.LABEL, contactMethod.label); + } + values.put(Contacts.ContactMethodsColumns.DATA, contactMethod.data); + values.put(Contacts.ContactMethodsColumns.ISPRIMARY, 1); + values.put(Contacts.ContactMethods.PERSON_ID, personId); + + if (contactMethod.kind == Contacts.KIND_EMAIL) { + Uri emailUri; + if (resolver != null) { + emailUri = resolver.insert(ContactMethods.CONTENT_URI, values); + } else { + emailUri = provider.nonTransactionalInsert( + ContactMethods.CONTENT_URI, values); + } + if (contactMethod.isPrimary) { + primaryEmailId = Long.parseLong(emailUri.getLastPathSegment()); + } + } else { // probably KIND_POSTAL + if (resolver != null) { + resolver.insert(ContactMethods.CONTENT_URI, values); + } else { + provider.nonTransactionalInsert( + ContactMethods.CONTENT_URI, values); + } + } + } + } + + if (extensionMap != null && extensionMap.size() > 0) { + ArrayList<ContentValues> contentValuesArray; + if (resolver != null) { + contentValuesArray = new ArrayList<ContentValues>(); + } else { + contentValuesArray = null; + } + for (Entry<String, List<String>> entry : extensionMap.entrySet()) { + String key = entry.getKey(); + List<String> list = entry.getValue(); + for (String value : list) { + ContentValues values = new ContentValues(); + values.put(Extensions.NAME, key); + values.put(Extensions.VALUE, value); + values.put(Extensions.PERSON_ID, personId); + if (resolver != null) { + contentValuesArray.add(values); + } else { + provider.nonTransactionalInsert(Extensions.CONTENT_URI, values); + } + } + } + if (resolver != null) { + resolver.bulkInsert(Extensions.CONTENT_URI, + contentValuesArray.toArray(new ContentValues[0])); + } + } + + if (primaryPhoneId >= 0 || primaryOrganizationId >= 0 || primaryEmailId >= 0) { + ContentValues values = new ContentValues(); + if (primaryPhoneId >= 0) { + values.put(People.PRIMARY_PHONE_ID, primaryPhoneId); + } + if (primaryOrganizationId >= 0) { + values.put(People.PRIMARY_ORGANIZATION_ID, primaryOrganizationId); + } + if (primaryEmailId >= 0) { + values.put(People.PRIMARY_EMAIL_ID, primaryEmailId); + } + if (resolver != null) { + resolver.update(personUri, values, null, null); + } else { + provider.nonTransactionalUpdate(personUri, values, null, null); + } + } + } + + /** + * Push this object into database in the resolver. + */ + public void pushIntoContentResolver(ContentResolver resolver) { + pushIntoContentProviderOrResolver(resolver, 0); + } + + /** + * Push this object into AbstractSyncableContentProvider object. + */ + public void pushIntoAbstractSyncableContentProvider( + AbstractSyncableContentProvider provider, long myContactsGroupId) { + boolean successful = false; + provider.beginTransaction(); + try { + pushIntoContentProviderOrResolver(provider, myContactsGroupId); + successful = true; + } finally { + provider.endTransaction(successful); + } + } + + public boolean isIgnorable() { + return TextUtils.isEmpty(name) && + TextUtils.isEmpty(phoneticName) && + (phoneList == null || phoneList.size() == 0) && + (contactmethodList == null || contactmethodList.size() == 0); } } diff --git a/core/java/android/syncml/pim/vcard/VCardComposer.java b/core/java/android/syncml/pim/vcard/VCardComposer.java index 05e8f40..192736a 100644 --- a/core/java/android/syncml/pim/vcard/VCardComposer.java +++ b/core/java/android/syncml/pim/vcard/VCardComposer.java @@ -124,9 +124,9 @@ public class VCardComposer { mResult.append("ORG:").append(struct.company).append(mNewline); } - if (!isNull(struct.notes)) { + if (struct.notes.size() > 0 && !isNull(struct.notes.get(0))) { mResult.append("NOTE:").append( - foldingString(struct.notes, vcardversion)).append(mNewline); + foldingString(struct.notes.get(0), vcardversion)).append(mNewline); } if (!isNull(struct.title)) { @@ -190,7 +190,7 @@ public class VCardComposer { */ private void appendPhotoStr(byte[] bytes, String type, int version) throws VCardException { - String value, apptype, encodingStr; + String value, encodingStr; try { value = foldingString(new String(Base64.encodeBase64(bytes, true)), version); @@ -198,20 +198,23 @@ public class VCardComposer { throw new VCardException(e.getMessage()); } - if (isNull(type)) { - type = "image/jpeg"; - } - if (type.indexOf("jpeg") > 0) { - apptype = "JPEG"; - } else if (type.indexOf("gif") > 0) { - apptype = "GIF"; - } else if (type.indexOf("bmp") > 0) { - apptype = "BMP"; + if (isNull(type) || type.toUpperCase().indexOf("JPEG") >= 0) { + type = "JPEG"; + } else if (type.toUpperCase().indexOf("GIF") >= 0) { + type = "GIF"; + } else if (type.toUpperCase().indexOf("BMP") >= 0) { + type = "BMP"; } else { - apptype = type.substring(type.indexOf("/")).toUpperCase(); + // Handle the string like "image/tiff". + int indexOfSlash = type.indexOf("/"); + if (indexOfSlash >= 0) { + type = type.substring(indexOfSlash + 1).toUpperCase(); + } else { + type = type.toUpperCase(); + } } - mResult.append("LOGO;TYPE=").append(apptype); + mResult.append("LOGO;TYPE=").append(type); if (version == VERSION_VCARD21_INT) { encodingStr = ";ENCODING=BASE64:"; value = value + mNewline; @@ -281,7 +284,7 @@ public class VCardComposer { private String getPhoneTypeStr(PhoneData phone) { - int phoneType = Integer.parseInt(phone.type); + int phoneType = phone.type; String typeStr, label; if (phoneTypeMap.containsKey(phoneType)) { @@ -308,7 +311,7 @@ public class VCardComposer { String joinMark = version == VERSION_VCARD21_INT ? ";" : ","; for (ContactStruct.ContactMethod contactMethod : contactMList) { // same with v2.1 and v3.0 - switch (Integer.parseInt(contactMethod.kind)) { + switch (contactMethod.kind) { case Contacts.KIND_EMAIL: String mailType = "INTERNET"; if (!isNull(contactMethod.data)) { diff --git a/core/java/android/syncml/pim/vcard/VCardDataBuilder.java b/core/java/android/syncml/pim/vcard/VCardDataBuilder.java new file mode 100644 index 0000000..a0513f1 --- /dev/null +++ b/core/java/android/syncml/pim/vcard/VCardDataBuilder.java @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.syncml.pim.vcard; + +import android.app.ProgressDialog; +import android.content.AbstractSyncableContentProvider; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.IContentProvider; +import android.os.Handler; +import android.provider.Contacts; +import android.syncml.pim.PropertyNode; +import android.syncml.pim.VBuilder; +import android.syncml.pim.VNode; +import android.syncml.pim.VParser; +import android.util.CharsetUtils; +import android.util.Log; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.net.QuotedPrintableCodec; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +/** + * VBuilder for VCard. VCard may contain big photo images encoded by BASE64, + * If we store all VNode entries in memory like VDataBuilder.java, + * OutOfMemoryError may be thrown. Thus, this class push each VCard entry into + * ContentResolver immediately. + */ +public class VCardDataBuilder implements VBuilder { + static private String LOG_TAG = "VCardDataBuilder"; + + /** + * If there's no other information available, this class uses this charset for encoding + * byte arrays. + */ + static public String DEFAULT_CHARSET = "UTF-8"; + + private class ProgressShower implements Runnable { + private ContactStruct mContact; + + public ProgressShower(ContactStruct contact) { + mContact = contact; + } + + public void run () { + mProgressDialog.setMessage(mProgressMessage + "\n" + + mContact.displayString()); + } + } + + /** type=VNode */ + private VNode mCurrentVNode; + private PropertyNode mCurrentPropNode; + private String mCurrentParamType; + + /** + * The charset using which VParser parses the text. + */ + private String mSourceCharset; + + /** + * The charset with which byte array is encoded to String. + */ + private String mTargetCharset; + private boolean mStrictLineBreakParsing; + private ContentResolver mContentResolver; + + // For letting VCardDataBuilder show the display name of VCard while handling it. + private Handler mHandler; + private ProgressDialog mProgressDialog; + private String mProgressMessage; + private Runnable mOnProgressRunnable; + private boolean mLastNameComesBeforeFirstName; + + // Just for testing. + private long mTimeCreateContactStruct; + private long mTimePushIntoContentResolver; + + // Ideally, this should be ContactsProvider but it seems Class loader cannot find it, + // even when it is subclass of ContactsProvider... + private AbstractSyncableContentProvider mProvider; + private long mMyContactsGroupId; + + public VCardDataBuilder(ContentResolver resolver) { + mTargetCharset = DEFAULT_CHARSET; + mContentResolver = resolver; + } + + /** + * Constructor which requires minimum requiredvariables. + * + * @param resolver insert each data into this ContentResolver + * @param progressDialog + * @param progressMessage + * @param handler if this importer works on the different thread than main one, + * set appropriate handler object. If not, it is ok to set this null. + */ + public VCardDataBuilder(ContentResolver resolver, + ProgressDialog progressDialog, + String progressMessage, + Handler handler) { + this(resolver, progressDialog, progressMessage, handler, + null, null, false, false); + } + + public VCardDataBuilder(ContentResolver resolver, + ProgressDialog progressDialog, + String progressMessage, + Handler handler, + String charset, + boolean strictLineBreakParsing, + boolean lastNameComesBeforeFirstName) { + this(resolver, progressDialog, progressMessage, handler, + null, charset, strictLineBreakParsing, + lastNameComesBeforeFirstName); + } + + /** + * @hide + */ + public VCardDataBuilder(ContentResolver resolver, + ProgressDialog progressDialog, + String progressMessage, + Handler handler, + String sourceCharset, + String targetCharset, + boolean strictLineBreakParsing, + boolean lastNameComesBeforeFirstName) { + if (sourceCharset != null) { + mSourceCharset = sourceCharset; + } else { + mSourceCharset = VParser.DEFAULT_CHARSET; + } + if (targetCharset != null) { + mTargetCharset = targetCharset; + } else { + mTargetCharset = DEFAULT_CHARSET; + } + mContentResolver = resolver; + mStrictLineBreakParsing = strictLineBreakParsing; + mHandler = handler; + mProgressDialog = progressDialog; + mProgressMessage = progressMessage; + mLastNameComesBeforeFirstName = lastNameComesBeforeFirstName; + + tryGetOriginalProvider(); + } + + private void tryGetOriginalProvider() { + final ContentResolver resolver = mContentResolver; + + if ((mMyContactsGroupId = Contacts.People.tryGetMyContactsGroupId(resolver)) == 0) { + Log.e(LOG_TAG, "Could not get group id of MyContact"); + return; + } + + IContentProvider iProviderForName = resolver.acquireProvider(Contacts.CONTENT_URI); + ContentProvider contentProvider = + ContentProvider.coerceToLocalContentProvider(iProviderForName); + if (contentProvider == null) { + Log.e(LOG_TAG, "Fail to get ContentProvider object."); + return; + } + + if (!(contentProvider instanceof AbstractSyncableContentProvider)) { + Log.e(LOG_TAG, + "Acquired ContentProvider object is not AbstractSyncableContentProvider."); + return; + } + + mProvider = (AbstractSyncableContentProvider)contentProvider; + } + + public void setOnProgressRunnable(Runnable runnable) { + mOnProgressRunnable = runnable; + } + + public void start() { + } + + public void end() { + } + + /** + * Assume that VCard is not nested. In other words, this code does not accept + */ + public void startRecord(String type) { + if (mCurrentVNode != null) { + // This means startRecord() is called inside startRecord() - endRecord() block. + // TODO: should throw some Exception + Log.e(LOG_TAG, "Nested VCard code is not supported now."); + } + mCurrentVNode = new VNode(); + mCurrentVNode.parseStatus = 1; + mCurrentVNode.VName = type; + } + + public void endRecord() { + mCurrentVNode.parseStatus = 0; + long start = System.currentTimeMillis(); + ContactStruct contact = ContactStruct.constructContactFromVNode(mCurrentVNode, + mLastNameComesBeforeFirstName ? ContactStruct.NAME_ORDER_TYPE_JAPANESE : + ContactStruct.NAME_ORDER_TYPE_ENGLISH); + mTimeCreateContactStruct += System.currentTimeMillis() - start; + if (!contact.isIgnorable()) { + if (mProgressDialog != null && mProgressMessage != null) { + if (mHandler != null) { + mHandler.post(new ProgressShower(contact)); + } else { + mProgressDialog.setMessage(mProgressMessage + "\n" + + contact.displayString()); + } + } + start = System.currentTimeMillis(); + if (mProvider != null) { + contact.pushIntoAbstractSyncableContentProvider( + mProvider, mMyContactsGroupId); + } else { + contact.pushIntoContentResolver(mContentResolver); + } + mTimePushIntoContentResolver += System.currentTimeMillis() - start; + } + if (mOnProgressRunnable != null) { + mOnProgressRunnable.run(); + } + mCurrentVNode = null; + } + + public void startProperty() { + mCurrentPropNode = new PropertyNode(); + } + + public void endProperty() { + mCurrentVNode.propList.add(mCurrentPropNode); + mCurrentPropNode = null; + } + + public void propertyName(String name) { + mCurrentPropNode.propName = name; + } + + public void propertyGroup(String group) { + mCurrentPropNode.propGroupSet.add(group); + } + + public void propertyParamType(String type) { + mCurrentParamType = type; + } + + public void propertyParamValue(String value) { + if (mCurrentParamType == null || + mCurrentParamType.equalsIgnoreCase("TYPE")) { + mCurrentPropNode.paramMap_TYPE.add(value); + } else { + mCurrentPropNode.paramMap.put(mCurrentParamType, value); + } + + mCurrentParamType = null; + } + + private String encodeString(String originalString, String targetCharset) { + if (mSourceCharset.equalsIgnoreCase(targetCharset)) { + return originalString; + } + Charset charset = Charset.forName(mSourceCharset); + ByteBuffer byteBuffer = charset.encode(originalString); + // byteBuffer.array() "may" return byte array which is larger than + // byteBuffer.remaining(). Here, we keep on the safe side. + byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + try { + return new String(bytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return new String(bytes); + } + } + + private String handleOneValue(String value, String targetCharset, String encoding) { + if (encoding != null) { + if (encoding.equals("BASE64") || encoding.equals("B")) { + mCurrentPropNode.propValue_bytes = + Base64.decodeBase64(value.getBytes()); + return value; + } else if (encoding.equals("QUOTED-PRINTABLE")) { + // "= " -> " ", "=\t" -> "\t". + // Previous code had done this replacement. Keep on the safe side. + StringBuilder builder = new StringBuilder(); + int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (ch == '=' && i < length - 1) { + char nextCh = value.charAt(i + 1); + if (nextCh == ' ' || nextCh == '\t') { + + builder.append(nextCh); + i++; + continue; + } + } + builder.append(ch); + } + String quotedPrintable = builder.toString(); + + String[] lines; + if (mStrictLineBreakParsing) { + lines = quotedPrintable.split("\r\n"); + } else { + builder = new StringBuilder(); + length = quotedPrintable.length(); + ArrayList<String> list = new ArrayList<String>(); + for (int i = 0; i < length; i++) { + char ch = quotedPrintable.charAt(i); + if (ch == '\n') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else if (ch == '\r') { + list.add(builder.toString()); + builder = new StringBuilder(); + if (i < length - 1) { + char nextCh = quotedPrintable.charAt(i + 1); + if (nextCh == '\n') { + i++; + } + } + } else { + builder.append(ch); + } + } + String finalLine = builder.toString(); + if (finalLine.length() > 0) { + list.add(finalLine); + } + lines = list.toArray(new String[0]); + } + + builder = new StringBuilder(); + for (String line : lines) { + if (line.endsWith("=")) { + line = line.substring(0, line.length() - 1); + } + builder.append(line); + } + byte[] bytes; + try { + bytes = builder.toString().getBytes(mSourceCharset); + } catch (UnsupportedEncodingException e1) { + Log.e(LOG_TAG, "Failed to encode: charset=" + mSourceCharset); + bytes = builder.toString().getBytes(); + } + + try { + bytes = QuotedPrintableCodec.decodeQuotedPrintable(bytes); + } catch (DecoderException e) { + Log.e(LOG_TAG, "Failed to decode quoted-printable: " + e); + return ""; + } + + try { + return new String(bytes, targetCharset); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + return new String(bytes); + } + } + // Unknown encoding. Fall back to default. + } + return encodeString(value, targetCharset); + } + + public void propertyValues(List<String> values) { + if (values == null || values.size() == 0) { + mCurrentPropNode.propValue_bytes = null; + mCurrentPropNode.propValue_vector.clear(); + mCurrentPropNode.propValue_vector.add(""); + mCurrentPropNode.propValue = ""; + return; + } + + ContentValues paramMap = mCurrentPropNode.paramMap; + + String targetCharset = CharsetUtils.nameForDefaultVendor(paramMap.getAsString("CHARSET")); + String encoding = paramMap.getAsString("ENCODING"); + + if (targetCharset == null || targetCharset.length() == 0) { + targetCharset = mTargetCharset; + } + + for (String value : values) { + mCurrentPropNode.propValue_vector.add( + handleOneValue(value, targetCharset, encoding)); + } + + mCurrentPropNode.propValue = listToString(mCurrentPropNode.propValue_vector); + } + + public void showDebugInfo() { + Log.d(LOG_TAG, "time for creating ContactStruct: " + mTimeCreateContactStruct + " ms"); + Log.d(LOG_TAG, "time for insert ContactStruct to database: " + + mTimePushIntoContentResolver + " ms"); + } + + private String listToString(List<String> list){ + int size = list.size(); + if (size > 1) { + StringBuilder builder = new StringBuilder(); + int i = 0; + for (String type : list) { + builder.append(type); + if (i < size - 1) { + builder.append(";"); + } + } + return builder.toString(); + } else if (size == 1) { + return list.get(0); + } else { + return ""; + } + } +} diff --git a/core/java/android/syncml/pim/vcard/VCardEntryCounter.java b/core/java/android/syncml/pim/vcard/VCardEntryCounter.java new file mode 100644 index 0000000..03cd1d9 --- /dev/null +++ b/core/java/android/syncml/pim/vcard/VCardEntryCounter.java @@ -0,0 +1,63 @@ +/* + * 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.syncml.pim.vcard; + +import java.util.List; + +import android.syncml.pim.VBuilder; + +public class VCardEntryCounter implements VBuilder { + private int mCount; + + public int getCount() { + return mCount; + } + + public void start() { + } + + public void end() { + } + + public void startRecord(String type) { + } + + public void endRecord() { + mCount++; + } + + public void startProperty() { + } + + public void endProperty() { + } + + public void propertyGroup(String group) { + } + + public void propertyName(String name) { + } + + public void propertyParamType(String type) { + } + + public void propertyParamValue(String value) { + } + + public void propertyValues(List<String> values) { + } +}
\ No newline at end of file diff --git a/core/java/android/syncml/pim/vcard/VCardNestedException.java b/core/java/android/syncml/pim/vcard/VCardNestedException.java new file mode 100644 index 0000000..def6f3b --- /dev/null +++ b/core/java/android/syncml/pim/vcard/VCardNestedException.java @@ -0,0 +1,27 @@ +/* + * 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.syncml.pim.vcard; + +/** + * VCardException thrown when VCard is nested without VCardParser's being notified. + */ +public class VCardNestedException extends VCardException { + public VCardNestedException() {} + public VCardNestedException(String message) { + super(message); + } +} diff --git a/core/java/android/syncml/pim/vcard/VCardParser_V21.java b/core/java/android/syncml/pim/vcard/VCardParser_V21.java index f853c5e..d865668 100644 --- a/core/java/android/syncml/pim/vcard/VCardParser_V21.java +++ b/core/java/android/syncml/pim/vcard/VCardParser_V21.java @@ -17,21 +17,26 @@ package android.syncml.pim.vcard; import android.syncml.pim.VBuilder; +import android.syncml.pim.VParser; +import android.util.Log; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.Reader; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; -import java.util.regex.Pattern; /** - * This class is used to parse vcard. Please refer to vCard Specification 2.1 + * This class is used to parse vcard. Please refer to vCard Specification 2.1. */ public class VCardParser_V21 { - + private static final String LOG_TAG = "VCardParser_V21"; + + public static final String DEFAULT_CHARSET = VParser.DEFAULT_CHARSET; + /** Store the known-type */ private static final HashSet<String> sKnownTypeSet = new HashSet<String>( Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", @@ -42,19 +47,17 @@ public class VCardParser_V21 { "CGM", "WMF", "BMP", "MET", "PMB", "DIB", "PICT", "TIFF", "PDF", "PS", "JPEG", "QTIME", "MPEG", "MPEG2", "AVI", "WAVE", "AIFF", "PCM", "X509", "PGP")); - + /** Store the known-value */ private static final HashSet<String> sKnownValueSet = new HashSet<String>( Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); - /** Store the property name available in vCard 2.1 */ - // NICKNAME is not supported in vCard 2.1, but some vCard may contain. + /** Store the property names available in vCard 2.1 */ private static final HashSet<String> sAvailablePropertyNameV21 = new HashSet<String>(Arrays.asList( - "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", + "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", - "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", - "NICKNAME")); + "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER")); // Though vCard 2.1 specification does not allow "B" encoding, some data may have it. // We allow it for safety... @@ -76,6 +79,30 @@ public class VCardParser_V21 { // Should not directly read a line from this. Use getLine() instead. protected BufferedReader mReader; + private boolean mCanceled; + + // In some cases, vCard is nested. Currently, we only consider the most interior vCard data. + // See v21_foma_1.vcf in test directory for more information. + private int mNestCount; + + // In order to reduce warning message as much as possible, we hold the value which made Logger + // emit a warning message. + protected HashSet<String> mWarningValueMap = new HashSet<String>(); + + // Just for debugging + private long mTimeTotal; + private long mTimeStartRecord; + private long mTimeEndRecord; + private long mTimeStartProperty; + private long mTimeEndProperty; + private long mTimeParseItems; + private long mTimeParseItem1; + private long mTimeParseItem2; + private long mTimeParseItem3; + private long mTimeHandlePropertyValue1; + private long mTimeHandlePropertyValue2; + private long mTimeHandlePropertyValue3; + /** * Create a new VCard parser. */ @@ -83,12 +110,35 @@ public class VCardParser_V21 { super(); } + public VCardParser_V21(VCardSourceDetector detector) { + super(); + if (detector != null && detector.getType() == VCardSourceDetector.TYPE_FOMA) { + mNestCount = 1; + } + } + /** * Parse the file at the given position * vcard_file = [wsls] vcard [wsls] */ protected void parseVCardFile() throws IOException, VCardException { - while (parseOneVCard()) { + boolean firstReading = true; + while (true) { + if (mCanceled) { + break; + } + if (!parseOneVCard(firstReading)) { + break; + } + firstReading = false; + } + + if (mNestCount > 0) { + boolean useCache = true; + for (int i = 0; i < mNestCount; i++) { + readEndVCard(useCache, true); + useCache = false; + } } } @@ -100,7 +150,13 @@ public class VCardParser_V21 { * @return true when the propertyName is a valid property name. */ protected boolean isValidPropertyName(String propertyName) { - return sAvailablePropertyNameV21.contains(propertyName.toUpperCase()); + if (!(sAvailablePropertyNameV21.contains(propertyName.toUpperCase()) || + propertyName.startsWith("X-")) && + !mWarningValueMap.contains(propertyName)) { + mWarningValueMap.add(propertyName); + Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); + } + return true; } /** @@ -129,7 +185,7 @@ public class VCardParser_V21 { line = getLine(); if (line == null) { throw new VCardException("Reached end of buffer."); - } else if (line.trim().length() > 0) { + } else if (line.trim().length() > 0) { return line; } } @@ -140,12 +196,37 @@ public class VCardParser_V21 { * items *CRLF * "END" [ws] ":" [ws] "VCARD" */ - private boolean parseOneVCard() throws IOException, VCardException { - if (!readBeginVCard()) { + private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException { + boolean allowGarbage = false; + if (firstReading) { + if (mNestCount > 0) { + for (int i = 0; i < mNestCount; i++) { + if (!readBeginVCard(allowGarbage)) { + return false; + } + allowGarbage = true; + } + } + } + + if (!readBeginVCard(allowGarbage)) { return false; } + long start; + if (mBuilder != null) { + start = System.currentTimeMillis(); + mBuilder.startRecord("VCARD"); + mTimeStartRecord += System.currentTimeMillis() - start; + } + start = System.currentTimeMillis(); parseItems(); - readEndVCard(); + mTimeParseItems += System.currentTimeMillis() - start; + readEndVCard(true, false); + if (mBuilder != null) { + start = System.currentTimeMillis(); + mBuilder.endRecord(); + mTimeEndRecord += System.currentTimeMillis() - start; + } return true; } @@ -154,46 +235,102 @@ public class VCardParser_V21 { * @throws IOException * @throws VCardException */ - protected boolean readBeginVCard() throws IOException, VCardException { + protected boolean readBeginVCard(boolean allowGarbage) + throws IOException, VCardException { String line; - while (true) { - line = getLine(); - if (line == null) { - return false; - } else if (line.trim().length() > 0) { - break; + do { + while (true) { + line = getLine(); + if (line == null) { + return false; + } else if (line.trim().length() > 0) { + break; + } } - } - String[] strArray = line.split(":", 2); - - // Though vCard specification does not allow lower cases, - // some data may have them, so we allow it. - if (!(strArray.length == 2 && - strArray[0].trim().equalsIgnoreCase("BEGIN") && - strArray[1].trim().equalsIgnoreCase("VCARD"))) { - throw new VCardException("BEGIN:VCARD != \"" + line + "\""); - } - - if (mBuilder != null) { - mBuilder.startRecord("VCARD"); - } + String[] strArray = line.split(":", 2); + int length = strArray.length; - return true; + // Though vCard 2.1/3.0 specification does not allow lower cases, + // some data may have them, so we allow it (Actually, previous code + // had explicitly allowed "BEGIN:vCard" though there's no example). + // + // TODO: ignore non vCard entry (e.g. vcalendar). + // XXX: Not sure, but according to VDataBuilder.java, vcalendar + // entry + // may be nested. Just seeking "END:SOMETHING" may not be enough. + // e.g. + // BEGIN:VCARD + // ... (Valid. Must parse this) + // END:VCARD + // BEGIN:VSOMETHING + // ... (Must ignore this) + // BEGIN:VSOMETHING2 + // ... (Must ignore this) + // END:VSOMETHING2 + // ... (Must ignore this!) + // END:VSOMETHING + // BEGIN:VCARD + // ... (Valid. Must parse this) + // END:VCARD + // INVALID_STRING (VCardException should be thrown) + if (length == 2 && + strArray[0].trim().equalsIgnoreCase("BEGIN") && + strArray[1].trim().equalsIgnoreCase("VCARD")) { + return true; + } else if (!allowGarbage) { + if (mNestCount > 0) { + mPreviousLine = line; + return false; + } else { + throw new VCardException( + "Expected String \"BEGIN:VCARD\" did not come " + + "(Instead, \"" + line + "\" came)"); + } + } + } while(allowGarbage); + + throw new VCardException("Reached where must not be reached."); } - - protected void readEndVCard() throws VCardException { - // Though vCard specification does not allow lower cases, - // some data may have them, so we allow it. - String[] strArray = mPreviousLine.split(":", 2); - if (!(strArray.length == 2 && - strArray[0].trim().equalsIgnoreCase("END") && - strArray[1].trim().equalsIgnoreCase("VCARD"))) { - throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); - } - - if (mBuilder != null) { - mBuilder.endRecord(); - } + + /** + * The arguments useCache and allowGarbase are usually true and false accordingly when + * this function is called outside this function itself. + * + * @param useCache When true, line is obtained from mPreviousline. Otherwise, getLine() + * is used. + * @param allowGarbage When true, ignore non "END:VCARD" line. + * @throws IOException + * @throws VCardException + */ + protected void readEndVCard(boolean useCache, boolean allowGarbage) + throws IOException, VCardException { + String line; + do { + if (useCache) { + // Though vCard specification does not allow lower cases, + // some data may have them, so we allow it. + line = mPreviousLine; + } else { + while (true) { + line = getLine(); + if (line == null) { + throw new VCardException("Expected END:VCARD was not found."); + } else if (line.trim().length() > 0) { + break; + } + } + } + + String[] strArray = line.split(":", 2); + if (strArray.length == 2 && + strArray[0].trim().equalsIgnoreCase("END") && + strArray[1].trim().equalsIgnoreCase("VCARD")) { + return; + } else if (!allowGarbage) { + throw new VCardException("END:VCARD != \"" + mPreviousLine + "\""); + } + useCache = false; + } while (allowGarbage); } /** @@ -205,32 +342,33 @@ public class VCardParser_V21 { boolean ended = false; if (mBuilder != null) { + long start = System.currentTimeMillis(); mBuilder.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - start; } - - try { - ended = parseItem(); - } finally { - if (mBuilder != null) { - mBuilder.endProperty(); - } + ended = parseItem(); + if (mBuilder != null && !ended) { + long start = System.currentTimeMillis(); + mBuilder.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - start; } while (!ended) { // follow VCARD ,it wont reach endProperty if (mBuilder != null) { + long start = System.currentTimeMillis(); mBuilder.startProperty(); + mTimeStartProperty += System.currentTimeMillis() - start; } - try { - ended = parseItem(); - } finally { - if (mBuilder != null) { - mBuilder.endProperty(); - } + ended = parseItem(); + if (mBuilder != null && !ended) { + long start = System.currentTimeMillis(); + mBuilder.endProperty(); + mTimeEndProperty += System.currentTimeMillis() - start; } } } - + /** * item = [groups "."] name [params] ":" value CRLF * / [groups "."] "ADR" [params] ":" addressparts CRLF @@ -241,57 +379,134 @@ public class VCardParser_V21 { protected boolean parseItem() throws IOException, VCardException { mEncoding = sDefaultEncoding; - // params = ";" [ws] paramlist String line = getNonEmptyLine(); - String[] strArray = line.split(":", 2); - if (strArray.length < 2) { - throw new VCardException("Invalid line(\":\" does not exist): " + line); - } - String propertyValue = strArray[1]; - String[] groupNameParamsArray = strArray[0].split(";"); - String groupAndName = groupNameParamsArray[0].trim(); - String[] groupNameArray = groupAndName.split("\\."); - int length = groupNameArray.length; - String propertyName = groupNameArray[length - 1]; - if (mBuilder != null) { - mBuilder.propertyName(propertyName); - for (int i = 0; i < length - 1; i++) { - mBuilder.propertyGroup(groupNameArray[i]); - } - } - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; + long start = System.currentTimeMillis(); + + String[] propertyNameAndValue = separateLineAndHandleGroup(line); + if (propertyNameAndValue == null) { return true; } - - length = groupNameParamsArray.length; - for (int i = 1; i < length; i++) { - handleParams(groupNameParamsArray[i]); + if (propertyNameAndValue.length != 2) { + throw new VCardException("Invalid line \"" + line + "\""); } - - if (isValidPropertyName(propertyName) || - propertyName.startsWith("X-")) { - if (propertyName.equals("VERSION") && - !propertyValue.equals(getVersion())) { - throw new VCardVersionException("Incompatible version: " + - propertyValue + " != " + getVersion()); - } - handlePropertyValue(propertyName, propertyValue); - return false; - } else if (propertyName.equals("ADR") || + String propertyName = propertyNameAndValue[0].toUpperCase(); + String propertyValue = propertyNameAndValue[1]; + + mTimeParseItem1 += System.currentTimeMillis() - start; + + if (propertyName.equals("ADR") || propertyName.equals("ORG") || propertyName.equals("N")) { + start = System.currentTimeMillis(); handleMultiplePropertyValue(propertyName, propertyValue); + mTimeParseItem3 += System.currentTimeMillis() - start; return false; } else if (propertyName.equals("AGENT")) { handleAgent(propertyValue); return false; + } else if (isValidPropertyName(propertyName)) { + if (propertyName.equals("BEGIN")) { + if (propertyValue.equals("VCARD")) { + throw new VCardNestedException("This vCard has nested vCard data in it."); + } else { + throw new VCardException("Unknown BEGIN type: " + propertyValue); + } + } else if (propertyName.equals("VERSION") && + !propertyValue.equals(getVersion())) { + throw new VCardVersionException("Incompatible version: " + + propertyValue + " != " + getVersion()); + } + start = System.currentTimeMillis(); + handlePropertyValue(propertyName, propertyValue); + mTimeParseItem2 += System.currentTimeMillis() - start; + return false; } throw new VCardException("Unknown property name: \"" + propertyName + "\""); } + static private final int STATE_GROUP_OR_PROPNAME = 0; + static private final int STATE_PARAMS = 1; + // vCard 3.1 specification allows double-quoted param-value, while vCard 2.1 does not. + // This is just for safety. + static private final int STATE_PARAMS_IN_DQUOTE = 2; + + protected String[] separateLineAndHandleGroup(String line) throws VCardException { + int length = line.length(); + int state = STATE_GROUP_OR_PROPNAME; + int nameIndex = 0; + + String[] propertyNameAndValue = new String[2]; + + for (int i = 0; i < length; i++) { + char ch = line.charAt(i); + switch (state) { + case STATE_GROUP_OR_PROPNAME: + if (ch == ':') { + String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + if (mBuilder != null) { + mBuilder.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } else if (ch == '.') { + String groupName = line.substring(nameIndex, i); + if (mBuilder != null) { + mBuilder.propertyGroup(groupName); + } + nameIndex = i + 1; + } else if (ch == ';') { + String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + if (mBuilder != null) { + mBuilder.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + nameIndex = i + 1; + state = STATE_PARAMS; + } + break; + case STATE_PARAMS: + if (ch == '"') { + state = STATE_PARAMS_IN_DQUOTE; + } else if (ch == ';') { + handleParams(line.substring(nameIndex, i)); + nameIndex = i + 1; + } else if (ch == ':') { + handleParams(line.substring(nameIndex, i)); + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } + break; + case STATE_PARAMS_IN_DQUOTE: + if (ch == '"') { + state = STATE_PARAMS; + } + break; + } + } + + throw new VCardException("Invalid line: \"" + line + "\""); + } + + /** * params = ";" [ws] paramlist * paramlist = paramlist [ws] ";" [ws] param @@ -330,18 +545,19 @@ public class VCardParser_V21 { } /** - * typeval = knowntype / "X-" word + * ptypeval = knowntype / "X-" word */ - protected void handleType(String ptypeval) throws VCardException { - if (sKnownTypeSet.contains(ptypeval.toUpperCase()) || - ptypeval.startsWith("X-")) { - if (mBuilder != null) { - mBuilder.propertyParamType("TYPE"); - mBuilder.propertyParamValue(ptypeval.toUpperCase()); - } - } else { - throw new VCardException("Unknown type: \"" + ptypeval + "\""); - } + protected void handleType(String ptypeval) { + String upperTypeValue = ptypeval; + if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && + !mWarningValueMap.contains(ptypeval)) { + mWarningValueMap.add(ptypeval); + Log.w(LOG_TAG, "Type unsupported by vCard 2.1: " + ptypeval); + } + if (mBuilder != null) { + mBuilder.propertyParamType("TYPE"); + mBuilder.propertyParamValue(upperTypeValue); + } } /** @@ -427,31 +643,48 @@ public class VCardParser_V21 { protected void handlePropertyValue( String propertyName, String propertyValue) throws IOException, VCardException { - if (mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") - || mEncoding.equalsIgnoreCase("8BIT") - || mEncoding.toUpperCase().startsWith("X-")) { - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(maybeUnescapeText(propertyValue)); - mBuilder.propertyValues(v); - } - } else if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { + if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { + long start = System.currentTimeMillis(); String result = getQuotedPrintable(propertyValue); if (mBuilder != null) { ArrayList<String> v = new ArrayList<String>(); v.add(result); mBuilder.propertyValues(v); } + mTimeHandlePropertyValue2 += System.currentTimeMillis() - start; } else if (mEncoding.equalsIgnoreCase("BASE64") || mEncoding.equalsIgnoreCase("B")) { - String result = getBase64(propertyValue); + long start = System.currentTimeMillis(); + // It is very rare, but some BASE64 data may be so big that + // OutOfMemoryError occurs. To ignore such cases, use try-catch. + try { + String result = getBase64(propertyValue); + if (mBuilder != null) { + ArrayList<String> v = new ArrayList<String>(); + v.add(result); + mBuilder.propertyValues(v); + } + } catch (OutOfMemoryError error) { + Log.e(LOG_TAG, "OutOfMemoryError happened during parsing BASE64 data!"); + if (mBuilder != null) { + mBuilder.propertyValues(null); + } + } + mTimeHandlePropertyValue3 += System.currentTimeMillis() - start; + } else { + if (!(mEncoding == null || mEncoding.equalsIgnoreCase("7BIT") + || mEncoding.equalsIgnoreCase("8BIT") + || mEncoding.toUpperCase().startsWith("X-"))) { + Log.w(LOG_TAG, "The encoding unsupported by vCard spec: \"" + mEncoding + "\"."); + } + + long start = System.currentTimeMillis(); if (mBuilder != null) { ArrayList<String> v = new ArrayList<String>(); - v.add(result); + v.add(maybeUnescapeText(propertyValue)); mBuilder.propertyValues(v); - } - } else { - throw new VCardException("Unknown encoding: \"" + mEncoding + "\""); + } + mTimeHandlePropertyValue1 += System.currentTimeMillis() - start; } } @@ -546,57 +779,51 @@ public class VCardParser_V21 { if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { propertyValue = getQuotedPrintable(propertyValue); } - - if (propertyValue.endsWith("\\")) { + + if (mBuilder != null) { + // TODO: limit should be set in accordance with propertyName? StringBuilder builder = new StringBuilder(); - // builder.append(propertyValue); - builder.append(propertyValue.substring(0, propertyValue.length() - 1)); - try { - String line; - while (true) { - line = getNonEmptyLine(); - // builder.append("\r\n"); - // builder.append(line); - if (!line.endsWith("\\")) { - builder.append(line); - break; + ArrayList<String> list = new ArrayList<String>(); + int length = propertyValue.length(); + for (int i = 0; i < length; i++) { + char ch = propertyValue.charAt(i); + if (ch == '\\' && i < length - 1) { + char nextCh = propertyValue.charAt(i + 1); + String unescapedString = maybeUnescape(nextCh); + if (unescapedString != null) { + builder.append(unescapedString); + i++; } else { - builder.append(line.substring(0, line.length() - 1)); + builder.append(ch); } + } else if (ch == ';') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else { + builder.append(ch); } - } catch (IOException e) { - throw new VCardException( - "IOException is throw during reading propertyValue" + e); } - // Now, propertyValue may contain "\r\n" - propertyValue = builder.toString(); - } - - if (mBuilder != null) { - // In String#replaceAll() and Pattern class, "\\\\" means single slash. - - final String IMPOSSIBLE_STRING = "\0"; - // First replace two backslashes with impossible strings. - propertyValue = propertyValue.replaceAll("\\\\\\\\", IMPOSSIBLE_STRING); - - // Now, split propertyValue with ; whose previous char is not back slash. - Pattern pattern = Pattern.compile("(?<!\\\\);"); - // TODO: limit should be set in accordance with propertyName? - String[] strArray = pattern.split(propertyValue, -1); - ArrayList<String> arrayList = new ArrayList<String>(); - for (String str : strArray) { - // Replace impossible strings with original two backslashes - arrayList.add( - unescapeText(str.replaceAll(IMPOSSIBLE_STRING, "\\\\\\\\"))); - } - mBuilder.propertyValues(arrayList); + list.add(builder.toString()); + mBuilder.propertyValues(list); } } /** * vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all. + * + * item = ... + * / [groups "."] "AGENT" + * [params] ":" vcard CRLF + * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF + * items *CRLF "END" [ws] ":" [ws] "VCARD" + * */ - protected void handleAgent(String propertyValue) throws IOException, VCardException { + protected void handleAgent(String propertyValue) throws VCardException { + throw new VCardException("AGENT Property is not supported."); + /* This is insufficient support. Also, AGENT Property is very rare. + Ignore it for now. + TODO: fix this. + String[] strArray = propertyValue.split(":", 2); if (!(strArray.length == 2 || strArray[0].trim().equalsIgnoreCase("BEGIN") && @@ -605,6 +832,7 @@ public class VCardParser_V21 { } parseItems(); readEndVCard(); + */ } /** @@ -615,17 +843,18 @@ public class VCardParser_V21 { } /** - * Convert escaped text into unescaped text. + * Returns unescaped String if the character should be unescaped. Return null otherwise. + * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. */ - protected String unescapeText(String text) { + protected String maybeUnescape(char ch) { // Original vCard 2.1 specification does not allow transformation // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of // this class allowed them, so keep it as is. - // In String#replaceAll(), "\\\\" means single slash. - return text.replaceAll("\\\\;", ";") - .replaceAll("\\\\:", ":") - .replaceAll("\\\\,", ",") - .replaceAll("\\\\\\\\", "\\\\"); + if (ch == '\\' || ch == ';' || ch == ':' || ch == ',') { + return String.valueOf(ch); + } else { + return null; + } } /** @@ -656,12 +885,15 @@ public class VCardParser_V21 { */ public boolean parse(InputStream is, String charset, VBuilder builder) throws IOException, VCardException { + // TODO: make this count error entries instead of just throwing VCardException. + // TODO: If we really need to allow only CRLF as line break, // we will have to develop our own BufferedReader(). - mReader = new BufferedReader(new InputStreamReader(is, charset)); + mReader = new CustomBufferedReader(new InputStreamReader(is, charset)); mBuilder = builder; + long start = System.currentTimeMillis(); if (mBuilder != null) { mBuilder.start(); } @@ -669,9 +901,50 @@ public class VCardParser_V21 { if (mBuilder != null) { mBuilder.end(); } + mTimeTotal += System.currentTimeMillis() - start; + return true; } + public boolean parse(InputStream is, VBuilder builder) throws IOException, VCardException { + return parse(is, DEFAULT_CHARSET, builder); + } + + /** + * Cancel parsing. + * Actual cancel is done after the end of the current one vcard entry parsing. + */ + public void cancel() { + mCanceled = true; + } + + /** + * It is very, very rare case, but there is a case where + * canceled may be already true outside this object. + * @hide + */ + public void parse(InputStream is, String charset, VBuilder builder, boolean canceled) + throws IOException, VCardException { + mCanceled = canceled; + parse(is, charset, builder); + } + + public void showDebugInfo() { + Log.d(LOG_TAG, "total parsing time: " + mTimeTotal + " ms"); + if (mReader instanceof CustomBufferedReader) { + Log.d(LOG_TAG, "total readLine time: " + + ((CustomBufferedReader)mReader).getTotalmillisecond() + " ms"); + } + Log.d(LOG_TAG, "mTimeStartRecord: " + mTimeStartRecord + " ms"); + Log.d(LOG_TAG, "mTimeEndRecord: " + mTimeEndRecord + " ms"); + Log.d(LOG_TAG, "mTimeParseItem1: " + mTimeParseItem1 + " ms"); + Log.d(LOG_TAG, "mTimeParseItem2: " + mTimeParseItem2 + " ms"); + Log.d(LOG_TAG, "mTimeParseItem3: " + mTimeParseItem3 + " ms"); + Log.d(LOG_TAG, "mTimeHandlePropertyValue1: " + mTimeHandlePropertyValue1 + " ms"); + Log.d(LOG_TAG, "mTimeHandlePropertyValue2: " + mTimeHandlePropertyValue2 + " ms"); + Log.d(LOG_TAG, "mTimeHandlePropertyValue3: " + mTimeHandlePropertyValue3 + " ms"); + } + private boolean isLetter(char ch) { if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { return true; @@ -679,3 +952,24 @@ public class VCardParser_V21 { return false; } } + +class CustomBufferedReader extends BufferedReader { + private long mTime; + + public CustomBufferedReader(Reader in) { + super(in); + } + + @Override + public String readLine() throws IOException { + long start = System.currentTimeMillis(); + String ret = super.readLine(); + long end = System.currentTimeMillis(); + mTime += end - start; + return ret; + } + + public long getTotalmillisecond() { + return mTime; + } +} diff --git a/core/java/android/syncml/pim/vcard/VCardParser_V30.java b/core/java/android/syncml/pim/vcard/VCardParser_V30.java index 901bd49..e67525e 100644 --- a/core/java/android/syncml/pim/vcard/VCardParser_V30.java +++ b/core/java/android/syncml/pim/vcard/VCardParser_V30.java @@ -16,8 +16,9 @@ package android.syncml.pim.vcard; +import android.util.Log; + import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -26,9 +27,11 @@ import java.util.HashSet; * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426) */ public class VCardParser_V30 extends VCardParser_V21 { + private static final String LOG_TAG = "VCardParser_V30"; + private static final HashSet<String> acceptablePropsWithParam = new HashSet<String>( Arrays.asList( - "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", + "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", "VERSION", "TEL", "EMAIL", "TZ", "GEO", "NOTE", "URL", "BDAY", "ROLE", "REV", "UID", "KEY", "MAILER", // 2.1 "NAME", "PROFILE", "SOURCE", "NICKNAME", "CLASS", @@ -51,8 +54,14 @@ public class VCardParser_V30 extends VCardParser_V21 { @Override protected boolean isValidPropertyName(String propertyName) { - return acceptablePropsWithParam.contains(propertyName) || - acceptablePropsWithoutParam.contains(propertyName); + if (!(acceptablePropsWithParam.contains(propertyName) || + acceptablePropsWithoutParam.contains(propertyName) || + propertyName.startsWith("X-")) && + !mWarningValueMap.contains(propertyName)) { + mWarningValueMap.add(propertyName); + Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName); + } + return true; } @Override @@ -100,7 +109,21 @@ public class VCardParser_V30 extends VCardParser_V21 { } } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') { if (builder != null) { - // TODO: Check whether MIME requires only one whitespace. + // See Section 5.8.1 of RFC 2425 (MIME-DIR document). + // Following is the excerpts from it. + // + // DESCRIPTION:This is a long description that exists on a long line. + // + // Can be represented as: + // + // DESCRIPTION:This is a long description + // that exists on a long line. + // + // It could also be represented as: + // + // DESCRIPTION:This is a long descrip + // tion that exists o + // n a long line. builder.append(line.substring(1)); } else if (mPreviousLine != null) { builder = new StringBuilder(); @@ -113,10 +136,13 @@ public class VCardParser_V30 extends VCardParser_V21 { } else { if (mPreviousLine == null) { mPreviousLine = line; + if (builder != null) { + return builder.toString(); + } } else { String ret = mPreviousLine; mPreviousLine = line; - return ret; + return ret; } } } @@ -130,15 +156,16 @@ public class VCardParser_V30 extends VCardParser_V21 { * [group "."] "END" ":" "VCARD" 1*CRLF */ @Override - protected boolean readBeginVCard() throws IOException, VCardException { + protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { // TODO: vCard 3.0 supports group. - return super.readBeginVCard(); + return super.readBeginVCard(allowGarbage); } @Override - protected void readEndVCard() throws VCardException { + protected void readEndVCard(boolean useCache, boolean allowGarbage) + throws IOException, VCardException { // TODO: vCard 3.0 supports group. - super.readEndVCard(); + super.readEndVCard(useCache, allowGarbage); } /** @@ -214,23 +241,6 @@ public class VCardParser_V30 extends VCardParser_V21 { throw new VCardException("AGENT in vCard 3.0 is not supported yet."); } - // vCard 3.0 supports "B" as BASE64 encoding. - @Override - protected void handlePropertyValue( - String propertyName, String propertyValue) throws - IOException, VCardException { - if (mEncoding != null && mEncoding.equalsIgnoreCase("B")) { - String result = getBase64(propertyValue); - if (mBuilder != null) { - ArrayList<String> v = new ArrayList<String>(); - v.add(result); - mBuilder.propertyValues(v); - } - } - - super.handlePropertyValue(propertyName, propertyValue); - } - /** * vCard 3.0 does not require two CRLF at the last of BASE64 data. * It only requires that data should be MIME-encoded. @@ -259,27 +269,38 @@ public class VCardParser_V30 extends VCardParser_V21 { } /** - * Return unescapeText(text). - * In vCard 3.0, 8bit text is always encoded. - */ - @Override - protected String maybeUnescapeText(String text) { - return unescapeText(text); - } - - /** * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N") * ; \\ encodes \, \n or \N encodes newline * ; \; encodes ;, \, encodes , - */ + * + * Note: Apple escape ':' into '\:' while does not escape '\' + */ @Override - protected String unescapeText(String text) { - // In String#replaceAll(), "\\\\" means single slash. - return text.replaceAll("\\\\;", ";") - .replaceAll("\\\\:", ":") - .replaceAll("\\\\,", ",") - .replaceAll("\\\\n", "\r\n") - .replaceAll("\\\\N", "\r\n") - .replaceAll("\\\\\\\\", "\\\\"); + protected String maybeUnescapeText(String text) { + StringBuilder builder = new StringBuilder(); + int length = text.length(); + for (int i = 0; i < length; i++) { + char ch = text.charAt(i); + if (ch == '\\' && i < length - 1) { + char next_ch = text.charAt(++i); + if (next_ch == 'n' || next_ch == 'N') { + builder.append("\r\n"); + } else { + builder.append(next_ch); + } + } else { + builder.append(ch); + } + } + return builder.toString(); + } + + @Override + protected String maybeUnescape(char ch) { + if (ch == 'n' || ch == 'N') { + return "\r\n"; + } else { + return String.valueOf(ch); + } } } diff --git a/core/java/android/syncml/pim/vcard/VCardSourceDetector.java b/core/java/android/syncml/pim/vcard/VCardSourceDetector.java new file mode 100644 index 0000000..8c48391 --- /dev/null +++ b/core/java/android/syncml/pim/vcard/VCardSourceDetector.java @@ -0,0 +1,140 @@ +/* + * 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.syncml.pim.vcard; + +import android.syncml.pim.VBuilder; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class which tries to detects the source of the vCard from its properties. + * Currently this implementation is very premature. + * @hide + */ +public class VCardSourceDetector implements VBuilder { + // Should only be used in package. + static final int TYPE_UNKNOWN = 0; + static final int TYPE_APPLE = 1; + static final int TYPE_JAPANESE_MOBILE_PHONE = 2; // Used in Japanese mobile phones. + static final int TYPE_FOMA = 3; // Used in some Japanese FOMA mobile phones. + static final int TYPE_WINDOWS_MOBILE_JP = 4; + // TODO: Excel, etc. + + private static Set<String> APPLE_SIGNS = new HashSet<String>(Arrays.asList( + "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", "X-PHONETIC-LAST-NAME", + "X-ABADR", "X-ABUID")); + + private static Set<String> JAPANESE_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList( + "X-GNO", "X-GN", "X-REDUCTION")); + + private static Set<String> WINDOWS_MOBILE_PHONE_SIGNS = new HashSet<String>(Arrays.asList( + "X-MICROSOFT-ASST_TEL", "X-MICROSOFT-ASSISTANT", "X-MICROSOFT-OFFICELOC")); + + // Note: these signes appears before the signs of the other type (e.g. "X-GN"). + // In other words, Japanese FOMA mobile phones are detected as FOMA, not JAPANESE_MOBILE_PHONES. + private static Set<String> FOMA_SIGNS = new HashSet<String>(Arrays.asList( + "X-SD-VERN", "X-SD-FORMAT_VER", "X-SD-CATEGORIES", "X-SD-CLASS", "X-SD-DCREATED", + "X-SD-DESCRIPTION")); + private static String TYPE_FOMA_CHARSET_SIGN = "X-SD-CHAR_CODE"; + + private int mType = TYPE_UNKNOWN; + // Some mobile phones (like FOMA) tells us the charset of the data. + private boolean mNeedParseSpecifiedCharset; + private String mSpecifiedCharset; + + public void start() { + } + + public void end() { + } + + public void startRecord(String type) { + } + + public void startProperty() { + mNeedParseSpecifiedCharset = false; + } + + public void endProperty() { + } + + public void endRecord() { + } + + public void propertyGroup(String group) { + } + + public void propertyName(String name) { + if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) { + mType = TYPE_FOMA; + mNeedParseSpecifiedCharset = true; + return; + } + if (mType != TYPE_UNKNOWN) { + return; + } + if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) { + mType = TYPE_WINDOWS_MOBILE_JP; + } else if (FOMA_SIGNS.contains(name)) { + mType = TYPE_FOMA; + } else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) { + mType = TYPE_JAPANESE_MOBILE_PHONE; + } else if (APPLE_SIGNS.contains(name)) { + mType = TYPE_APPLE; + } + } + + public void propertyParamType(String type) { + } + + public void propertyParamValue(String value) { + } + + public void propertyValues(List<String> values) { + if (mNeedParseSpecifiedCharset && values.size() > 0) { + mSpecifiedCharset = values.get(0); + } + } + + int getType() { + return mType; + } + + /** + * Return charset String guessed from the source's properties. + * This method must be called after parsing target file(s). + * @return Charset String. Null is returned if guessing the source fails. + */ + public String getEstimatedCharset() { + if (mSpecifiedCharset != null) { + return mSpecifiedCharset; + } + switch (mType) { + case TYPE_WINDOWS_MOBILE_JP: + case TYPE_FOMA: + case TYPE_JAPANESE_MOBILE_PHONE: + return "SHIFT_JIS"; + case TYPE_APPLE: + return "UTF-8"; + default: + return null; + } + } +} diff --git a/core/java/android/test/AndroidTestCase.java b/core/java/android/test/AndroidTestCase.java index 9bafa32..de0587a 100644 --- a/core/java/android/test/AndroidTestCase.java +++ b/core/java/android/test/AndroidTestCase.java @@ -16,12 +16,14 @@ package android.test; +import android.content.ContentValues; import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import junit.framework.TestCase; import java.lang.reflect.Field; -import junit.framework.TestCase; - /** * Extend this if you need to access Resources or other things that depend on Activity Context. */ @@ -53,6 +55,72 @@ public class AndroidTestCase extends TestCase { } /** + * Asserts that launching a given activity is protected by a particular permission by + * attempting to start the activity and validating that a {@link SecurityException} + * is thrown that mentions the permission in its error message. + * + * Note that an instrumentation isn't needed because all we are looking for is a security error + * and we don't need to wait for the activity to launch and get a handle to the activity. + * + * @param packageName The package name of the activity to launch. + * @param className The class of the activity to launch. + * @param permission The name of the permission. + */ + public void assertActivityRequiresPermission( + String packageName, String className, String permission) { + final Intent intent = new Intent(); + intent.setClassName(packageName, className); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + try { + getContext().startActivity(intent); + fail("expected security exception for " + permission); + } catch (SecurityException expected) { + assertNotNull("security exception's error message.", expected.getMessage()); + assertTrue("error message should contain " + permission + ".", + expected.getMessage().contains(permission)); + } + } + + + /** + * Asserts that reading from the content uri requires a particular permission by querying the + * uri and ensuring a {@link SecurityException} is thrown mentioning the particular permission. + * + * @param uri The uri that requires a permission to query. + * @param permission The permission that should be required. + */ + public void assertReadingContentUriRequiresPermission(Uri uri, String permission) { + try { + getContext().getContentResolver().query(uri, null, null, null, null); + fail("expected SecurityException requiring " + permission); + } catch (SecurityException expected) { + assertNotNull("security exception's error message.", expected.getMessage()); + assertTrue("error message should contain " + permission + ".", + expected.getMessage().contains(permission)); + } + } + + /** + * Asserts that writing to the content uri requires a particular permission by inserting into + * the uri and ensuring a {@link SecurityException} is thrown mentioning the particular + * permission. + * + * @param uri The uri that requires a permission to query. + * @param permission The permission that should be required. + */ + public void assertWritingContentUriRequiresPermission(Uri uri, String permission) { + try { + getContext().getContentResolver().insert(uri, new ContentValues()); + fail("expected SecurityException requiring " + permission); + } catch (SecurityException expected) { + assertNotNull("security exception's error message.", expected.getMessage()); + assertTrue("error message should contain " + permission + ".", + expected.getMessage().contains(permission)); + } + } + + /** * This function is called by various TestCase implementations, at tearDown() time, in order * to scrub out any class variables. This protects against memory leaks in the case where a * test case creates a non-static inner class (thus referencing the test case) and gives it to diff --git a/core/java/android/test/InstrumentationTestCase.java b/core/java/android/test/InstrumentationTestCase.java index 470ab0d..2145d7c 100644 --- a/core/java/android/test/InstrumentationTestCase.java +++ b/core/java/android/test/InstrumentationTestCase.java @@ -241,7 +241,13 @@ public class InstrumentationTestCase extends TestCase { try { final Field keyCodeField = KeyEvent.class.getField("KEYCODE_" + key); final int keyCode = keyCodeField.getInt(null); - instrumentation.sendKeyDownUpSync(keyCode); + try { + instrumentation.sendKeyDownUpSync(keyCode); + } catch (SecurityException e) { + // Ignore security exceptions that are now thrown + // when trying to send to another app, to retain + // compatibility with existing tests. + } } catch (NoSuchFieldException e) { Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key); break; @@ -266,7 +272,13 @@ public class InstrumentationTestCase extends TestCase { final Instrumentation instrumentation = getInstrumentation(); for (int i = 0; i < count; i++) { - instrumentation.sendKeyDownUpSync(keys[i]); + try { + instrumentation.sendKeyDownUpSync(keys[i]); + } catch (SecurityException e) { + // Ignore security exceptions that are now thrown + // when trying to send to another app, to retain + // compatibility with existing tests. + } } instrumentation.waitForIdleSync(); @@ -292,7 +304,13 @@ public class InstrumentationTestCase extends TestCase { final int keyCount = keys[i]; final int keyCode = keys[i + 1]; for (int j = 0; j < keyCount; j++) { - instrumentation.sendKeyDownUpSync(keyCode); + try { + instrumentation.sendKeyDownUpSync(keyCode); + } catch (SecurityException e) { + // Ignore security exceptions that are now thrown + // when trying to send to another app, to retain + // compatibility with existing tests. + } } } diff --git a/core/java/android/text/LoginFilter.java b/core/java/android/text/LoginFilter.java index 27c703f..9045c09 100644 --- a/core/java/android/text/LoginFilter.java +++ b/core/java/android/text/LoginFilter.java @@ -49,10 +49,6 @@ public abstract class LoginFilter implements InputFilter { */ public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { - char[] out = new char[end - start]; // reserve enough space for whole string - int outidx = 0; - boolean changed = false; - onStart(); // Scan through beginning characters in dest, calling onInvalidCharacter() @@ -63,14 +59,26 @@ public abstract class LoginFilter implements InputFilter { } // Scan through changed characters rejecting disallowed chars + SpannableStringBuilder modification = null; + int modoff = 0; + for (int i = start; i < end; i++) { char c = source.charAt(i); if (isAllowed(c)) { - // Character allowed. Add it to the sequence. - out[outidx++] = c; + // Character allowed. + modoff++; } else { - if (mAppendInvalid) out[outidx++] = c; - else changed = true; // we changed the original string + if (mAppendInvalid) { + modoff++; + } else { + if (modification == null) { + modification = new SpannableStringBuilder(source, start, end); + modoff = i - start; + } + + modification.delete(modoff, modoff + 1); + } + onInvalidCharacter(c); } } @@ -84,20 +92,9 @@ public abstract class LoginFilter implements InputFilter { onStop(); - if (!changed) { - return null; - } - - String s = new String(out, 0, outidx); - - if (source instanceof Spanned) { - SpannableString sp = new SpannableString(s); - TextUtils.copySpansFrom((Spanned) source, - start, end, null, sp, 0); - return sp; - } else { - return s; - } + // Either returns null if we made no changes, + // or what we wanted to change it to if there were changes. + return modification; } /** diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 5b4c380..53096dd 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -916,6 +916,17 @@ public class TextUtils { sp.setSpan(o, p.readInt(), p.readInt(), p.readInt()); } + /** + * Copies the spans from the region <code>start...end</code> in + * <code>source</code> to the region + * <code>destoff...destoff+end-start</code> in <code>dest</code>. + * Spans in <code>source</code> that begin before <code>start</code> + * or end after <code>end</code> but overlap this range are trimmed + * as if they began at <code>start</code> or ended at <code>end</code>. + * + * @throws IndexOutOfBoundsException if any of the copied spans + * are out of range in <code>dest</code>. + */ public static void copySpansFrom(Spanned source, int start, int end, Class kind, Spannable dest, int destoff) { diff --git a/core/java/android/text/format/DateFormat.java b/core/java/android/text/format/DateFormat.java index 0dc96c3..3d10f17 100644 --- a/core/java/android/text/format/DateFormat.java +++ b/core/java/android/text/format/DateFormat.java @@ -242,7 +242,7 @@ public class DateFormat { /** * Returns a {@link java.text.DateFormat} object that can format the time according - * to the current user preference. + * to the current locale and the user's 12-/24-hour clock preference. * @param context the application context * @return the {@link java.text.DateFormat} object that properly formats the time. */ @@ -260,46 +260,88 @@ public class DateFormat { } /** - * Returns a {@link java.text.DateFormat} object that can format the date according - * to the current user preference. + * Returns a {@link java.text.DateFormat} object that can format the date + * in short form (such as 12/31/1999) according + * to the current locale and the user's date-order preference. * @param context the application context * @return the {@link java.text.DateFormat} object that properly formats the date. */ public static final java.text.DateFormat getDateFormat(Context context) { - String value = getDateFormatString(context); + String value = Settings.System.getString(context.getContentResolver(), + Settings.System.DATE_FORMAT); + + return getDateFormatForSetting(context, value); + } + + /** + * Returns a {@link java.text.DateFormat} object to format the date + * as if the date format setting were set to <code>value</code>, + * including null to use the locale's default format. + * @param context the application context + * @param value the date format setting string to interpret for + * the current locale + * @hide + */ + public static java.text.DateFormat getDateFormatForSetting(Context context, + String value) { + if (value != null) { + int month = value.indexOf('M'); + int day = value.indexOf('d'); + int year = value.indexOf('y'); + + if (month >= 0 && day >= 0 && year >= 0) { + String template = context.getString(R.string.numeric_date_template); + if (year < month) { + if (month < day) { + value = String.format(template, "yyyy", "MM", "dd"); + } else { + value = String.format(template, "yyyy", "dd", "MM"); + } + } else if (month < day) { + if (day < year) { + value = String.format(template, "MM", "dd", "yyyy"); + } else { // unlikely + value = String.format(template, "MM", "yyyy", "dd"); + } + } else { // day < month + if (month < year) { + value = String.format(template, "dd", "MM", "yyyy"); + } else { // unlikely + value = String.format(template, "dd", "yyyy", "MM"); + } + } + + return new java.text.SimpleDateFormat(value); + } + } + + /* + * The setting is not set; use the default. + * We use a resource string here instead of just DateFormat.SHORT + * so that we get a four-digit year instead a two-digit year. + */ + value = context.getString(R.string.numeric_date_format); return new java.text.SimpleDateFormat(value); } /** * Returns a {@link java.text.DateFormat} object that can format the date - * in long form (such as December 31, 1999) based on user preference. + * in long form (such as December 31, 1999) for the current locale. * @param context the application context * @return the {@link java.text.DateFormat} object that formats the date in long form. */ public static final java.text.DateFormat getLongDateFormat(Context context) { - String value = getDateFormatString(context); - if (value.indexOf('M') < value.indexOf('d')) { - value = context.getString(R.string.full_date_month_first); - } else { - value = context.getString(R.string.full_date_day_first); - } - return new java.text.SimpleDateFormat(value); + return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG); } /** * Returns a {@link java.text.DateFormat} object that can format the date - * in medium form (such as Dec. 31, 1999) based on user preference. + * in medium form (such as Dec. 31, 1999) for the current locale. * @param context the application context * @return the {@link java.text.DateFormat} object that formats the date in long form. */ public static final java.text.DateFormat getMediumDateFormat(Context context) { - String value = getDateFormatString(context); - if (value.indexOf('M') < value.indexOf('d')) { - value = context.getString(R.string.medium_date_month_first); - } else { - value = context.getString(R.string.medium_date_day_first); - } - return new java.text.SimpleDateFormat(value); + return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM); } /** @@ -338,6 +380,12 @@ public class DateFormat { } private static String getDateFormatString(Context context) { + java.text.DateFormat df; + df = java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT); + if (df instanceof SimpleDateFormat) { + return ((SimpleDateFormat) df).toPattern(); + } + String value = Settings.System.getString(context.getContentResolver(), Settings.System.DATE_FORMAT); if (value == null || value.length() < 6) { diff --git a/core/java/android/text/format/DateUtils.java b/core/java/android/text/format/DateUtils.java index 8a7cdd9..1a4eb69 100644 --- a/core/java/android/text/format/DateUtils.java +++ b/core/java/android/text/format/DateUtils.java @@ -62,15 +62,6 @@ public class DateUtils com.android.internal.R.string.day_of_week_short_friday, com.android.internal.R.string.day_of_week_short_saturday, }; - private static final int[] sDaysShorter = new int[] { - com.android.internal.R.string.day_of_week_shorter_sunday, - com.android.internal.R.string.day_of_week_shorter_monday, - com.android.internal.R.string.day_of_week_shorter_tuesday, - com.android.internal.R.string.day_of_week_shorter_wednesday, - com.android.internal.R.string.day_of_week_shorter_thursday, - com.android.internal.R.string.day_of_week_shorter_friday, - com.android.internal.R.string.day_of_week_shorter_saturday, - }; private static final int[] sDaysShortest = new int[] { com.android.internal.R.string.day_of_week_shortest_sunday, com.android.internal.R.string.day_of_week_shortest_monday, @@ -80,6 +71,20 @@ public class DateUtils com.android.internal.R.string.day_of_week_shortest_friday, com.android.internal.R.string.day_of_week_shortest_saturday, }; + private static final int[] sMonthsStandaloneLong = new int [] { + com.android.internal.R.string.month_long_standalone_january, + com.android.internal.R.string.month_long_standalone_february, + com.android.internal.R.string.month_long_standalone_march, + com.android.internal.R.string.month_long_standalone_april, + com.android.internal.R.string.month_long_standalone_may, + com.android.internal.R.string.month_long_standalone_june, + com.android.internal.R.string.month_long_standalone_july, + com.android.internal.R.string.month_long_standalone_august, + com.android.internal.R.string.month_long_standalone_september, + com.android.internal.R.string.month_long_standalone_october, + com.android.internal.R.string.month_long_standalone_november, + com.android.internal.R.string.month_long_standalone_december, + }; private static final int[] sMonthsLong = new int [] { com.android.internal.R.string.month_long_january, com.android.internal.R.string.month_long_february, @@ -127,7 +132,7 @@ public class DateUtils com.android.internal.R.string.pm, }; private static Configuration sLastConfig; - private static String sStatusTimeFormat; + private static java.text.DateFormat sStatusTimeFormat; private static String sElapsedFormatMMSS; private static String sElapsedFormatHMMSS; @@ -142,6 +147,9 @@ public class DateUtils public static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60; public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24; public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7; + /** + * This constant is actually the length of 364 days, not of a year! + */ public static final long YEAR_IN_MILLIS = WEEK_IN_MILLIS * 52; // The following FORMAT_* symbols are used for specifying the format of @@ -171,8 +179,14 @@ public class DateUtils // Date and time format strings that are constant and don't need to be // translated. + /** + * This is not actually the preferred 24-hour date format in all locales. + */ public static final String HOUR_MINUTE_24 = "%H:%M"; public static final String MONTH_FORMAT = "%B"; + /** + * This is not actually a useful month name in all locales. + */ public static final String ABBREV_MONTH_FORMAT = "%b"; public static final String NUMERIC_MONTH_FORMAT = "%m"; public static final String MONTH_DAY_FORMAT = "%-d"; @@ -255,18 +269,15 @@ public class DateUtils * For use with the 'abbrev' parameter of {@link #getDayOfWeekString} and {@link #getMonthString}. * @more * <p>e.g. "Su" or "Jan" - * <p>In some languages, the results returned for LENGTH_SHORT may be the same as - * return for {@link #LENGTH_MEDIUM}. + * <p>In most languages, the results returned for LENGTH_SHORT will be the same as + * the results returned for {@link #LENGTH_MEDIUM}. */ public static final int LENGTH_SHORT = 30; /** * Request an even shorter abbreviated version of the name. - * For use with the 'abbrev' parameter of {@link #getDayOfWeekString} and {@link #getMonthString}. - * @more - * <p>e.g. "M", "Tu", "Th" or "J" - * <p>In some languages, the results returned for LENGTH_SHORTEST may be the same as - * return for {@link #LENGTH_SHORTER}. + * Do not use this. Currently this will always return the same result + * as {@link #LENGTH_SHORT}. */ public static final int LENGTH_SHORTER = 40; @@ -275,8 +286,8 @@ public class DateUtils * For use with the 'abbrev' parameter of {@link #getDayOfWeekString} and {@link #getMonthString}. * @more * <p>e.g. "S", "T", "T" or "J" - * <p>In some languages, the results returned for LENGTH_SHORTEST may be the same as - * return for {@link #LENGTH_SHORTER}. + * <p>In some languages, the results returned for LENGTH_SHORTEST will be the same as + * the results returned for {@link #LENGTH_SHORT}. */ public static final int LENGTH_SHORTEST = 50; @@ -284,9 +295,12 @@ public class DateUtils * Return a string for the day of the week. * @param dayOfWeek One of {@link Calendar#SUNDAY Calendar.SUNDAY}, * {@link Calendar#MONDAY Calendar.MONDAY}, etc. - * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_SHORT}, {@link #LENGTH_SHORTER} - * or {@link #LENGTH_SHORTEST}. For forward compatibility, anything else - * will return the same as {#LENGTH_MEDIUM}. + * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_SHORT}, + * {@link #LENGTH_MEDIUM}, or {@link #LENGTH_SHORTEST}. + * Note that in most languages, {@link #LENGTH_SHORT} + * will return the same as {@link #LENGTH_MEDIUM}. + * Undefined lengths will return {@link #LENGTH_MEDIUM} + * but may return something different in the future. * @throws IndexOutOfBoundsException if the dayOfWeek is out of bounds. */ public static String getDayOfWeekString(int dayOfWeek, int abbrev) { @@ -295,7 +309,7 @@ public class DateUtils case LENGTH_LONG: list = sDaysLong; break; case LENGTH_MEDIUM: list = sDaysMedium; break; case LENGTH_SHORT: list = sDaysShort; break; - case LENGTH_SHORTER: list = sDaysShorter; break; + case LENGTH_SHORTER: list = sDaysShort; break; case LENGTH_SHORTEST: list = sDaysShortest; break; default: list = sDaysMedium; break; } @@ -316,13 +330,14 @@ public class DateUtils } /** - * Return a localized string for the day of the week. + * Return a localized string for the month of the year. * @param month One of {@link Calendar#JANUARY Calendar.JANUARY}, * {@link Calendar#FEBRUARY Calendar.FEBRUARY}, etc. - * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_SHORT}, {@link #LENGTH_SHORTER} - * or {@link #LENGTH_SHORTEST}. For forward compatibility, anything else - * will return the same as {#LENGTH_MEDIUM}. - * @return Localized day of the week. + * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_MEDIUM}, + * or {@link #LENGTH_SHORTEST}. + * Undefined lengths will return {@link #LENGTH_MEDIUM} + * but may return something different in the future. + * @return Localized month of the year. */ public static String getMonthString(int month, int abbrev) { // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. @@ -344,6 +359,40 @@ public class DateUtils } /** + * Return a localized string for the month of the year, for + * contexts where the month is not formatted together with + * a day of the month. + * + * @param month One of {@link Calendar#JANUARY Calendar.JANUARY}, + * {@link Calendar#FEBRUARY Calendar.FEBRUARY}, etc. + * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_MEDIUM}, + * or {@link #LENGTH_SHORTEST}. + * Undefined lengths will return {@link #LENGTH_MEDIUM} + * but may return something different in the future. + * @return Localized month of the year. + * @hide Pending API council approval + */ + public static String getStandaloneMonthString(int month, int abbrev) { + // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER. + // This is a shortcut to not spam the translators with too many variations + // of the same string. If we find that in a language the distinction + // is necessary, we can can add more without changing this API. + int[] list; + switch (abbrev) { + case LENGTH_LONG: list = sMonthsStandaloneLong; + break; + case LENGTH_MEDIUM: list = sMonthsMedium; break; + case LENGTH_SHORT: list = sMonthsMedium; break; + case LENGTH_SHORTER: list = sMonthsMedium; break; + case LENGTH_SHORTEST: list = sMonthsShortest; break; + default: list = sMonthsMedium; break; + } + + Resources r = Resources.getSystem(); + return r.getString(list[month - Calendar.JANUARY]); + } + + /** * Returns a string describing the elapsed time since startTime. * @param startTime some time in the past. * @return a String object containing the elapsed time. @@ -572,7 +621,7 @@ public class DateUtils Configuration cfg = r.getConfiguration(); if (sLastConfig == null || !sLastConfig.equals(cfg)) { sLastConfig = cfg; - sStatusTimeFormat = r.getString(com.android.internal.R.string.status_bar_time_format); + sStatusTimeFormat = java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT); sElapsedFormatMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_mm_ss); sElapsedFormatHMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_h_mm_ss); } @@ -586,7 +635,7 @@ public class DateUtils */ public static final CharSequence timeString(long millis) { initFormatStrings(); - return DateFormat.format(sStatusTimeFormat, millis); + return sStatusTimeFormat.format(millis); } /** @@ -1066,7 +1115,9 @@ public class DateUtils * * <p> * If FORMAT_CAP_AMPM is set and 12-hour time is used, then the "AM" - * and "PM" are capitalized. + * and "PM" are capitalized. You should not use this flag + * because in some locales these terms cannot be capitalized, and in + * many others it doesn't make sense to do so even though it is possible. * * <p> * If FORMAT_NO_NOON is set and 12-hour time is used, then "12pm" is @@ -1074,15 +1125,19 @@ public class DateUtils * * <p> * If FORMAT_CAP_NOON is set and 12-hour time is used, then "Noon" is - * shown instead of "noon". + * shown instead of "noon". You should probably not use this flag + * because in many locales it will not make sense to capitalize + * the term. * * <p> * If FORMAT_NO_MIDNIGHT is set and 12-hour time is used, then "12am" is * shown instead of "midnight". * * <p> - * If FORMAT_CAP_NOON is set and 12-hour time is used, then "Midnight" is - * shown instead of "midnight". + * If FORMAT_CAP_MIDNIGHT is set and 12-hour time is used, then "Midnight" + * is shown instead of "midnight". You should probably not use this + * flag because in many locales it will not make sense to capitalize + * the term. * * <p> * If FORMAT_12HOUR is set and the time is shown, then the time is @@ -1224,8 +1279,8 @@ public class DateUtils use24Hour = DateFormat.is24HourFormat(context); } if (use24Hour) { - startTimeFormat = HOUR_MINUTE_24; - endTimeFormat = HOUR_MINUTE_24; + startTimeFormat = endTimeFormat = + res.getString(com.android.internal.R.string.hour_minute_24); } else { boolean abbrevTime = (flags & (FORMAT_ABBREV_TIME | FORMAT_ABBREV_ALL)) != 0; boolean capAMPM = (flags & FORMAT_CAP_AMPM) != 0; @@ -1392,7 +1447,8 @@ public class DateUtils if (numericDate) { monthFormat = NUMERIC_MONTH_FORMAT; } else if (abbrevMonth) { - monthFormat = ABBREV_MONTH_FORMAT; + monthFormat = + res.getString(com.android.internal.R.string.short_format_month); } else { monthFormat = MONTH_FORMAT; } diff --git a/core/java/android/text/format/Formatter.java b/core/java/android/text/format/Formatter.java index 1b30aa0..367b26c 100644 --- a/core/java/android/text/format/Formatter.java +++ b/core/java/android/text/format/Formatter.java @@ -59,9 +59,15 @@ public final class Formatter { result = result / 1024; } if (result < 100) { - return String.format("%.2f%s", result, context.getText(suffix).toString()); + String value = String.format("%.2f", result); + return context.getResources(). + getString(com.android.internal.R.string.fileSizeSuffix, + value, context.getString(suffix)); } - return String.format("%.0f%s", result, context.getText(suffix).toString()); + String value = String.format("%.0f", result); + return context.getResources(). + getString(com.android.internal.R.string.fileSizeSuffix, + value, context.getString(suffix)); } /** diff --git a/core/java/android/text/format/Time.java b/core/java/android/text/format/Time.java index daa99c2..8eae111 100644 --- a/core/java/android/text/format/Time.java +++ b/core/java/android/text/format/Time.java @@ -135,6 +135,7 @@ public class Time { private static Locale sLocale; private static String[] sShortMonths; private static String[] sLongMonths; + private static String[] sLongStandaloneMonths; private static String[] sShortWeekdays; private static String[] sLongWeekdays; private static String sTimeOnlyFormat; @@ -321,6 +322,20 @@ public class Time { r.getString(com.android.internal.R.string.month_long_november), r.getString(com.android.internal.R.string.month_long_december), }; + sLongStandaloneMonths = new String[] { + r.getString(com.android.internal.R.string.month_long_standalone_january), + r.getString(com.android.internal.R.string.month_long_standalone_february), + r.getString(com.android.internal.R.string.month_long_standalone_march), + r.getString(com.android.internal.R.string.month_long_standalone_april), + r.getString(com.android.internal.R.string.month_long_standalone_may), + r.getString(com.android.internal.R.string.month_long_standalone_june), + r.getString(com.android.internal.R.string.month_long_standalone_july), + r.getString(com.android.internal.R.string.month_long_standalone_august), + r.getString(com.android.internal.R.string.month_long_standalone_september), + r.getString(com.android.internal.R.string.month_long_standalone_october), + r.getString(com.android.internal.R.string.month_long_standalone_november), + r.getString(com.android.internal.R.string.month_long_standalone_december), + }; sShortWeekdays = new String[] { r.getString(com.android.internal.R.string.day_of_week_medium_sunday), r.getString(com.android.internal.R.string.day_of_week_medium_monday), @@ -438,6 +453,7 @@ public class Time { * * @param s the string to parse * @return true if the resulting time value is in UTC time + * @throws android.util.TimeFormatException if s cannot be parsed. */ public boolean parse3339(String s) { if (nativeParse3339(s)) { diff --git a/core/java/android/text/method/DialerKeyListener.java b/core/java/android/text/method/DialerKeyListener.java index b121e60..584e83f 100644 --- a/core/java/android/text/method/DialerKeyListener.java +++ b/core/java/android/text/method/DialerKeyListener.java @@ -106,7 +106,7 @@ public class DialerKeyListener extends NumberKeyListener */ public static final char[] CHARACTERS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*', - '+', '-', '(', ')', ',', '/', 'N', '.', ' ' + '+', '-', '(', ')', ',', '/', 'N', '.', ' ', ';' }; private static DialerKeyListener sInstance; diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java index f2fb9cb..dfc16f5 100644 --- a/core/java/android/text/method/Touch.java +++ b/core/java/android/text/method/Touch.java @@ -81,6 +81,12 @@ public class Touch { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: + ds = buffer.getSpans(0, buffer.length(), DragState.class); + + for (int i = 0; i < ds.length; i++) { + buffer.removeSpan(ds[i]); + } + buffer.setSpan(new DragState(event.getX(), event.getY(), widget.getScrollX(), widget.getScrollY()), 0, 0, Spannable.SPAN_MARK_MARK); diff --git a/core/java/android/util/CharsetUtils.java b/core/java/android/util/CharsetUtils.java index 7553029..9d91aca 100644 --- a/core/java/android/util/CharsetUtils.java +++ b/core/java/android/util/CharsetUtils.java @@ -142,20 +142,25 @@ public final class CharsetUtils { /** * Returns whether the given character set name indicates the Shift-JIS - * encoding. + * encoding. Returns false if the name is null. * * @param charsetName the character set name * @return {@code true} if the name corresponds to Shift-JIS or * {@code false} if not */ private static boolean isShiftJis(String charsetName) { - if (charsetName.length() != 9) { - // Bail quickly if the length doesn't match. + // Bail quickly if the length doesn't match. + if (charsetName == null) { + return false; + } + int length = charsetName.length(); + if (length != 4 && length != 9) { return false; } return charsetName.equalsIgnoreCase("shift_jis") - || charsetName.equalsIgnoreCase("shift-jis"); + || charsetName.equalsIgnoreCase("shift-jis") + || charsetName.equalsIgnoreCase("sjis"); } /** diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java index e4dd020..4179edb 100644 --- a/core/java/android/util/DisplayMetrics.java +++ b/core/java/android/util/DisplayMetrics.java @@ -16,6 +16,8 @@ package android.util; +import android.content.res.CompatibilityInfo; +import android.content.res.Configuration; import android.os.*; @@ -35,8 +37,7 @@ public class DisplayMetrics { * The device's density. * @hide */ - public static final int DEVICE_DENSITY = SystemProperties.getInt("ro.sf.lcd_density", - DEFAULT_DENSITY); + public static final int DEVICE_DENSITY = getDeviceDensity(); /** * The absolute width of the display in pixels. @@ -101,22 +102,83 @@ public class DisplayMetrics { } /** - * Set the display metrics' density and update parameters depend on it. - * @hide + * Update the display metrics based on the compatibility info and orientation + * NOTE: DO NOT EXPOSE THIS API! It is introducing a circular dependency + * with the higher-level android.res package. + * {@hide} */ - public void updateDensity(float newDensity) { - float ratio = newDensity / density; - density = newDensity; - scaledDensity = density; - widthPixels *= ratio; - heightPixels *= ratio; - xdpi *= ratio; - ydpi *= ratio; + public void updateMetrics(CompatibilityInfo compatibilityInfo, int orientation, + int screenLayout) { + int xOffset = 0; + if (!compatibilityInfo.isConfiguredExpandable()) { + // Note: this assume that configuration is updated before calling + // updateMetrics method. + if (screenLayout == Configuration.SCREENLAYOUT_LARGE) { + // This is a large screen device and the app is not + // compatible with large screens, to diddle it. + + compatibilityInfo.setExpandable(false); + // Figure out the compatibility width and height of the screen. + int defaultWidth; + int defaultHeight; + switch (orientation) { + case Configuration.ORIENTATION_LANDSCAPE: { + defaultWidth = (int)(CompatibilityInfo.DEFAULT_PORTRAIT_HEIGHT * density); + defaultHeight = (int)(CompatibilityInfo.DEFAULT_PORTRAIT_WIDTH * density); + break; + } + case Configuration.ORIENTATION_PORTRAIT: + case Configuration.ORIENTATION_SQUARE: + default: { + defaultWidth = (int)(CompatibilityInfo.DEFAULT_PORTRAIT_WIDTH * density); + defaultHeight = (int)(CompatibilityInfo.DEFAULT_PORTRAIT_HEIGHT * density); + break; + } + case Configuration.ORIENTATION_UNDEFINED: { + // don't change + return; + } + } + + if (defaultWidth < widthPixels) { + // content/window's x offset in original pixels + xOffset = ((widthPixels - defaultWidth) / 2); + widthPixels = defaultWidth; + } + if (defaultHeight < heightPixels) { + heightPixels = defaultHeight; + } + + } else { + // the screen size is same as expected size. make it expandable + compatibilityInfo.setExpandable(true); + } + } + compatibilityInfo.setVisibleRect(xOffset, widthPixels, heightPixels); + if (compatibilityInfo.isScalingRequired()) { + float invertedRatio = compatibilityInfo.applicationInvertedScale; + density *= invertedRatio; + scaledDensity *= invertedRatio; + xdpi *= invertedRatio; + ydpi *= invertedRatio; + widthPixels *= invertedRatio; + heightPixels *= invertedRatio; + } } + @Override public String toString() { return "DisplayMetrics{density=" + density + ", width=" + widthPixels + ", height=" + heightPixels + ", scaledDensity=" + scaledDensity + ", xdpi=" + xdpi + ", ydpi=" + ydpi + "}"; } + + private static int getDeviceDensity() { + // qemu.sf.lcd_density can be used to override ro.sf.lcd_density + // when running in the emulator, allowing for dynamic configurations. + // The reason for this is that ro.sf.lcd_density is write-once and is + // set by the init process when it parses build.prop before anything else. + return SystemProperties.getInt("qemu.sf.lcd_density", + SystemProperties.getInt("ro.sf.lcd_density", DEFAULT_DENSITY)); + } } diff --git a/core/java/android/util/LongSparseArray.java b/core/java/android/util/LongSparseArray.java new file mode 100644 index 0000000..d90045f --- /dev/null +++ b/core/java/android/util/LongSparseArray.java @@ -0,0 +1,342 @@ +/* + * 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.util; + +import com.android.internal.util.ArrayUtils; + +/** + * SparseArrays map longs to Objects. Unlike a normal array of Objects, + * there can be gaps in the indices. It is intended to be more efficient + * than using a HashMap to map Longs to Objects. + * + * @hide + */ +public class LongSparseArray<E> { + private static final Object DELETED = new Object(); + private boolean mGarbage = false; + + /** + * Creates a new SparseArray containing no mappings. + */ + public LongSparseArray() { + this(10); + } + + /** + * Creates a new SparseArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. + */ + public LongSparseArray(int initialCapacity) { + initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity); + + mKeys = new long[initialCapacity]; + mValues = new Object[initialCapacity]; + mSize = 0; + } + + /** + * Gets the Object mapped from the specified key, or <code>null</code> + * if no such mapping has been made. + */ + public E get(long key) { + return get(key, null); + } + + /** + * Gets the Object mapped from the specified key, or the specified Object + * if no such mapping has been made. + */ + public E get(long key, E valueIfKeyNotFound) { + int i = binarySearch(mKeys, 0, mSize, key); + + if (i < 0 || mValues[i] == DELETED) { + return valueIfKeyNotFound; + } else { + return (E) mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(long key) { + int i = binarySearch(mKeys, 0, mSize, key); + + if (i >= 0) { + if (mValues[i] != DELETED) { + mValues[i] = DELETED; + mGarbage = true; + } + } + } + + /** + * Alias for {@link #delete(long)}. + */ + public void remove(long key) { + delete(key); + } + + private void gc() { + // Log.e("SparseArray", "gc start with " + mSize); + + int n = mSize; + int o = 0; + long[] keys = mKeys; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + Object val = values[i]; + + if (val != DELETED) { + if (i != o) { + keys[o] = keys[i]; + values[o] = val; + } + + o++; + } + } + + mGarbage = false; + mSize = o; + + // Log.e("SparseArray", "gc end with " + mSize); + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(long key, E value) { + int i = binarySearch(mKeys, 0, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + if (i < mSize && mValues[i] == DELETED) { + mKeys[i] = key; + mValues[i] = value; + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + + // Search again because indices may have changed. + i = ~binarySearch(mKeys, 0, mSize, key); + } + + if (mSize >= mKeys.length) { + int n = ArrayUtils.idealIntArraySize(mSize + 1); + + long[] nkeys = new long[n]; + Object[] nvalues = new Object[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + if (mSize - i != 0) { + // Log.e("SparseArray", "move " + (mSize - i)); + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + + mKeys[i] = key; + mValues[i] = value; + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseArray + * currently stores. + */ + public int size() { + if (mGarbage) { + gc(); + } + + return mSize; + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the key from the <code>index</code>th key-value mapping that this + * SparseArray stores. + */ + public long keyAt(int index) { + if (mGarbage) { + gc(); + } + + return mKeys[index]; + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the value from the <code>index</code>th key-value mapping that this + * SparseArray stores. + */ + public E valueAt(int index) { + if (mGarbage) { + gc(); + } + + return (E) mValues[index]; + } + + /** + * Given an index in the range <code>0...size()-1</code>, sets a new + * value for the <code>index</code>th key-value mapping that this + * SparseArray stores. + */ + public void setValueAt(int index, E value) { + if (mGarbage) { + gc(); + } + + mValues[index] = value; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(long key) { + if (mGarbage) { + gc(); + } + + return binarySearch(mKeys, 0, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(E value) { + if (mGarbage) { + gc(); + } + + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseArray. + */ + public void clear() { + int n = mSize; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + values[i] = null; + } + + mSize = 0; + mGarbage = false; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(long key, E value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + } + + int pos = mSize; + if (pos >= mKeys.length) { + int n = ArrayUtils.idealIntArraySize(pos + 1); + + long[] nkeys = new long[n]; + Object[] nvalues = new Object[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + mKeys[pos] = key; + mValues[pos] = value; + mSize = pos + 1; + } + + private static int binarySearch(long[] a, int start, int len, long key) { + int high = start + len, low = start - 1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + + if (a[guess] < key) + low = guess; + else + high = guess; + } + + if (high == start + len) + return ~(start + len); + else if (a[high] == key) + return high; + else + return ~high; + } + + private void checkIntegrity() { + for (int i = 1; i < mSize; i++) { + if (mKeys[i] <= mKeys[i - 1]) { + for (int j = 0; j < mSize; j++) { + Log.e("FAIL", j + ": " + mKeys[j] + " -> " + mValues[j]); + } + + throw new RuntimeException(); + } + } + } + + private long[] mKeys; + private Object[] mValues; + private int mSize; +}
\ No newline at end of file diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index 23f3e3c..1e558be 100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -198,6 +198,7 @@ public class GestureDetector { private int mTouchSlopSquare; private int mDoubleTapSlopSquare; private int mMinimumFlingVelocity; + private int mMaximumFlingVelocity; private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); @@ -361,11 +362,13 @@ public class GestureDetector { doubleTapSlop = ViewConfiguration.getDoubleTapSlop(); //noinspection deprecation mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity(); + mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity(); } else { final ViewConfiguration configuration = ViewConfiguration.get(context); touchSlop = configuration.getScaledTouchSlop(); doubleTapSlop = configuration.getScaledDoubleTapSlop(); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity(); } mTouchSlopSquare = touchSlop * touchSlop; mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; @@ -505,7 +508,7 @@ public class GestureDetector { // A fling must travel the minimum tap distance final VelocityTracker velocityTracker = mVelocityTracker; - velocityTracker.computeCurrentVelocity(1000); + velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); final float velocityY = velocityTracker.getYVelocity(); final float velocityX = velocityTracker.getXVelocity(); diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 86261c4..a224ed3 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -59,32 +59,32 @@ public final class MotionEvent implements Parcelable { public static final int ACTION_OUTSIDE = 4; private static final boolean TRACK_RECYCLED_LOCATION = false; - + /** * Flag indicating the motion event intersected the top edge of the screen. */ public static final int EDGE_TOP = 0x00000001; - + /** * Flag indicating the motion event intersected the bottom edge of the screen. */ public static final int EDGE_BOTTOM = 0x00000002; - + /** * Flag indicating the motion event intersected the left edge of the screen. */ public static final int EDGE_LEFT = 0x00000004; - + /** * Flag indicating the motion event intersected the right edge of the screen. */ public static final int EDGE_RIGHT = 0x00000008; - + static private final int MAX_RECYCLED = 10; static private Object gRecyclerLock = new Object(); static private int gRecyclerUsed = 0; static private MotionEvent gRecyclerTop = null; - + private long mDownTime; private long mEventTime; private int mAction; @@ -109,7 +109,7 @@ public final class MotionEvent implements Parcelable { private MotionEvent() { } - + static private MotionEvent obtain() { synchronized (gRecyclerLock) { if (gRecyclerTop == null) { @@ -123,26 +123,26 @@ public final class MotionEvent implements Parcelable { 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 + * + * @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 + * @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 -- one of either * {@link #ACTION_DOWN}, {@link #ACTION_MOVE}, {@link #ACTION_UP}, or * {@link #ACTION_CANCEL}. * @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 - * ranges from 0 (no pressure at all) to 1 (normal pressure), however - * values higher than 1 may be generated depending on the calibration of + * @param pressure The current pressure of this event. The pressure generally + * ranges from 0 (no pressure at all) to 1 (normal pressure), however + * values higher than 1 may be generated depending on the calibration of * the input device. * @param size A scaled value of the approximate size of the area being pressed when - * touched with the finger. The actual value in pixels corresponding to the finger + * touched with the finger. The actual value in pixels corresponding to the finger * touch is normalized with a device specific range of values * and scaled to a value between 0 and 1. * @param metaState The state of any meta / modifier keys that were in effect when @@ -174,15 +174,15 @@ public final class MotionEvent implements Parcelable { return ev; } - + /** * Create a new MotionEvent, filling in a subset of the basic motion * values. Those not specified here are: device id (always 0), pressure * and size (always 1), x and y precision (always 1), and edgeFlags (always 0). - * - * @param downTime The time (in ms) when the user originally pressed down to start + * + * @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 + * @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 -- one of either * {@link #ACTION_DOWN}, {@link #ACTION_MOVE}, {@link #ACTION_UP}, or @@ -212,27 +212,47 @@ public final class MotionEvent implements Parcelable { } /** - * Scales down the cood of this event by the given scale. + * Scales down the coordination of this event by the given scale. * * @hide */ public void scale(float scale) { - if (scale != 1.0f) { - mX *= scale; - mY *= scale; - mRawX *= scale; - mRawY *= scale; - mSize *= scale; - mXPrecision *= scale; - mYPrecision *= scale; - if (mHistory != null) { - float[] history = mHistory; - int length = history.length; - for (int i = 0; i < length; i += 4) { - history[i] *= scale; - history[i + 2] *= scale; - history[i + 3] *= scale; - } + mX *= scale; + mY *= scale; + mRawX *= scale; + mRawY *= scale; + mSize *= scale; + mXPrecision *= scale; + mYPrecision *= scale; + if (mHistory != null) { + float[] history = mHistory; + int length = history.length; + for (int i = 0; i < length; i += 4) { + history[i] *= scale; // X + history[i + 1] *= scale; // Y + // no need to scale pressure ([i+2]) + history[i + 3] *= scale; // Size, TODO: square this? + } + } + } + + /** + * Translate the coordination of the event by given x and y. + * + * @hide + */ + public void translate(float dx, float dy) { + mX += dx; + mY += dy; + mRawX += dx; + mRawY += dx; + if (mHistory != null) { + float[] history = mHistory; + int length = history.length; + for (int i = 0; i < length; i += 4) { + history[i] += dx; // X + history[i + 1] += dy; // Y + // no need to translate pressure (i+2) and size (i+3) } } } @@ -265,7 +285,7 @@ public final class MotionEvent implements Parcelable { } return ev; } - + /** * Recycle the MotionEvent, to be re-used by a later caller. After calling * this function you must not ever touch the event again. @@ -291,7 +311,7 @@ public final class MotionEvent implements Parcelable { } } } - + /** * Return the kind of action being performed -- one of either * {@link #ACTION_DOWN}, {@link #ACTION_MOVE}, {@link #ACTION_UP}, or @@ -302,8 +322,8 @@ public final class MotionEvent implements Parcelable { } /** - * Returns the time (in ms) when the user originally pressed down to start - * a stream of position events. + * Returns the time (in ms) when the user originally pressed down to start + * a stream of position events. */ public final long getDownTime() { return mDownTime; @@ -317,25 +337,25 @@ public final class MotionEvent implements Parcelable { } /** - * Returns the X coordinate of this event. Whole numbers are pixels; the - * value may have a fraction for input devices that are sub-pixel precise. + * Returns the X coordinate of this event. Whole numbers are pixels; the + * value may have a fraction for input devices that are sub-pixel precise. */ public final float getX() { return mX; } /** - * Returns the Y coordinate of this event. Whole numbers are pixels; the - * value may have a fraction for input devices that are sub-pixel precise. + * Returns the Y coordinate of this event. Whole numbers are pixels; the + * value may have a fraction for input devices that are sub-pixel precise. */ public final float getY() { return mY; } /** - * Returns the current pressure of this event. The pressure generally - * ranges from 0 (no pressure at all) to 1 (normal pressure), however - * values higher than 1 may be generated depending on the calibration of + * Returns the current pressure of this event. The pressure generally + * ranges from 0 (no pressure at all) to 1 (normal pressure), however + * values higher than 1 may be generated depending on the calibration of * the input device. */ public final float getPressure() { @@ -344,9 +364,9 @@ public final class MotionEvent implements Parcelable { /** * Returns a scaled value of the approximate size, of the area being pressed when - * touched with the finger. The actual value in pixels corresponding to the finger + * touched with the finger. The actual value in pixels corresponding to the finger * touch is normalized with the device specific range of values - * and scaled to a value between 0 and 1. The value of size can be used to + * and scaled to a value between 0 and 1. The value of size can be used to * determine fat touch events. */ public final float getSize() { @@ -396,7 +416,7 @@ public final class MotionEvent implements Parcelable { public final float getXPrecision() { return mXPrecision; } - + /** * Return the precision of the Y coordinates being reported. You can * multiple this number with {@link #getY} to find the actual hardware @@ -406,89 +426,89 @@ public final class MotionEvent implements Parcelable { public final float getYPrecision() { return mYPrecision; } - + /** * Returns the number of historical points in this event. These are * movements that have occurred between this event and the previous event. * This only applies to ACTION_MOVE events -- all other actions will have * a size of 0. - * + * * @return Returns the number of historical points in the event. */ public final int getHistorySize() { return mNumHistory; } - + /** * Returns the time that a historical movement occurred between this event * and the previous event. Only applies to ACTION_MOVE events. - * + * * @param pos Which historical value to return; must be less than * {@link #getHistorySize} - * + * * @see #getHistorySize * @see #getEventTime */ public final long getHistoricalEventTime(int pos) { return mHistoryTimes[pos]; } - + /** * Returns a historical X coordinate that occurred between this event * and the previous event. Only applies to ACTION_MOVE events. - * + * * @param pos Which historical value to return; must be less than * {@link #getHistorySize} - * + * * @see #getHistorySize * @see #getX */ public final float getHistoricalX(int pos) { return mHistory[pos*4]; } - + /** * Returns a historical Y coordinate that occurred between this event * and the previous event. Only applies to ACTION_MOVE events. - * + * * @param pos Which historical value to return; must be less than * {@link #getHistorySize} - * + * * @see #getHistorySize * @see #getY */ public final float getHistoricalY(int pos) { return mHistory[pos*4 + 1]; } - + /** * Returns a historical pressure coordinate that occurred between this event * and the previous event. Only applies to ACTION_MOVE events. - * + * * @param pos Which historical value to return; must be less than * {@link #getHistorySize} - * + * * @see #getHistorySize * @see #getPressure */ public final float getHistoricalPressure(int pos) { return mHistory[pos*4 + 2]; } - + /** * Returns a historical size coordinate that occurred between this event * and the previous event. Only applies to ACTION_MOVE events. - * + * * @param pos Which historical value to return; must be less than * {@link #getHistorySize} - * + * * @see #getHistorySize * @see #getSize */ public final float getHistoricalSize(int pos) { return mHistory[pos*4 + 3]; } - + /** * Return 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 @@ -497,12 +517,12 @@ public final class MotionEvent implements Parcelable { public final int getDeviceId() { return mDeviceId; } - + /** * Returns a bitfield indicating which edges, if any, where touched by this - * MotionEvent. For touch events, clients can use this to determine if the - * user's finger was touching the edge of the display. - * + * MotionEvent. For touch events, clients can use this to determine if the + * user's finger was touching the edge of the display. + * * @see #EDGE_LEFT * @see #EDGE_TOP * @see #EDGE_RIGHT @@ -511,12 +531,12 @@ public final class MotionEvent implements Parcelable { public final int getEdgeFlags() { return mEdgeFlags; } - + /** * Sets the bitfield indicating which edges, if any, where touched by this - * MotionEvent. - * + * MotionEvent. + * * @see #getEdgeFlags() */ public final void setEdgeFlags(int flags) { @@ -548,11 +568,11 @@ public final class MotionEvent implements Parcelable { pos[i+1] += deltaY; } } - + /** * Set this event's location. Applies {@link #offsetLocation} with a * delta from the current location to the given new location. - * + * * @param x New absolute X location. * @param y New absolute Y location. */ @@ -563,13 +583,13 @@ public final class MotionEvent implements Parcelable { offsetLocation(deltaX, deltaY); } } - + /** * Add a new movement to the batch of movements in this event. The event's * current location, position and size is updated to the new values. In * the future, the current values in the event will be added to a list of * historic values. - * + * * @param x The new X position. * @param y The new Y position. * @param pressure The new pressure. @@ -599,16 +619,16 @@ public final class MotionEvent implements Parcelable { mHistoryTimes = historyTimes = newHistoryTimes; } } - + historyTimes[N] = mEventTime; - + final int pos = N*4; history[pos] = mX; history[pos+1] = mY; history[pos+2] = mPressure; history[pos+3] = mSize; mNumHistory = N+1; - + mEventTime = eventTime; mX = mRawX = x; mY = mRawY = y; @@ -616,7 +636,7 @@ public final class MotionEvent implements Parcelable { mSize = size; mMetaState |= metaState; } - + @Override public String toString() { return "MotionEvent{" + Integer.toHexString(System.identityHashCode(this)) diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 3d023f7..45b0f0a 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -17,6 +17,8 @@ package android.view; import android.content.Context; +import android.content.res.CompatibilityInfo; +import android.content.res.CompatibilityInfo.Translator; import android.graphics.Canvas; import android.graphics.PixelFormat; import android.graphics.PorterDuff; @@ -100,6 +102,8 @@ public class SurfaceView extends View { static final int KEEP_SCREEN_ON_MSG = 1; static final int GET_NEW_SURFACE_MSG = 2; + int mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; + boolean mIsCreating = false; final Handler mHandler = new Handler() { @@ -135,28 +139,21 @@ public class SurfaceView extends View { int mFormat = -1; int mType = -1; final Rect mSurfaceFrame = new Rect(); - private final float mAppScale; - private final float mAppScaleInverted; + private Translator mTranslator; public SurfaceView(Context context) { super(context); setWillNotDraw(true); - mAppScale = context.getApplicationScale(); - mAppScaleInverted = 1.0f / mAppScale; } public SurfaceView(Context context, AttributeSet attrs) { super(context, attrs); setWillNotDraw(true); - mAppScale = context.getApplicationScale(); - mAppScaleInverted = 1.0f / mAppScale; } public SurfaceView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setWillNotDraw(true); - mAppScale = context.getApplicationScale(); - mAppScaleInverted = 1.0f / mAppScale; } /** @@ -259,9 +256,9 @@ public class SurfaceView extends View { public boolean dispatchTouchEvent(MotionEvent event) { // SurfaceView uses pre-scaled size unless fixed size is requested. This hook // scales the event back to the pre-scaled coordinates for such surface. - if (mRequestedWidth < 0 && mAppScale != 1.0f) { + if (mRequestedWidth < 0 && mTranslator != null) { MotionEvent scaledBack = MotionEvent.obtain(event); - scaledBack.scale(mAppScale); + scaledBack.scale(mTranslator.applicationScale); try { return super.dispatchTouchEvent(scaledBack); } finally { @@ -285,20 +282,33 @@ public class SurfaceView extends View { super.dispatchDraw(canvas); } + /** + * Hack to allow special layering of windows. The type is one of the + * types in WindowManager.LayoutParams. This is a hack so: + * @hide + */ + public void setWindowType(int type) { + mWindowType = type; + } + private void updateWindow(boolean force) { if (!mHaveFrame) { return; } + mTranslator = ((ViewRoot)getRootView().getParent()).mTranslator; + + float appScale = mTranslator == null ? 1.0f : mTranslator.applicationScale; int myWidth = mRequestedWidth; if (myWidth <= 0) myWidth = getWidth(); int myHeight = mRequestedHeight; if (myHeight <= 0) myHeight = getHeight(); - // Use original size for surface unless fixed size is requested. - if (mRequestedWidth <= 0) { - myWidth *= mAppScale; - myHeight *= mAppScale; + // Use original size if the app specified the size of the view, + // and let the flinger to scale up. + if (mRequestedWidth <= 0 && mTranslator != null && mTranslator.scalingRequired) { + myWidth *= appScale; + myHeight *= appScale; } getLocationInWindow(mLocation); @@ -316,7 +326,7 @@ public class SurfaceView extends View { + " visible=" + visibleChanged + " left=" + (mLeft != mLocation[0]) + " top=" + (mTop != mLocation[1])); - + try { final boolean visible = mVisible = mRequestedVisible; mLeft = mLocation[0]; @@ -326,23 +336,30 @@ public class SurfaceView extends View { mFormat = mRequestedFormat; mType = mRequestedType; - // Scaling window's layout here beause mLayout is not used elsewhere. - mLayout.x = (int) (mLeft * mAppScale); - mLayout.y = (int) (mTop * mAppScale); - mLayout.width = (int) (getWidth() * mAppScale); - mLayout.height = (int) (getHeight() * mAppScale); + // Scaling/Translate window's layout here because mLayout is not used elsewhere. + + // Places the window relative + mLayout.x = mLeft; + mLayout.y = mTop; + mLayout.width = getWidth(); + mLayout.height = getHeight(); + if (mTranslator != null) { + mTranslator.translateLayoutParamsInAppWindowToScreen(mLayout); + } + mLayout.format = mRequestedFormat; mLayout.flags |=WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS | WindowManager.LayoutParams.FLAG_SCALED | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NO_COMPATIBILITY_SCALING ; mLayout.memoryType = mRequestedType; if (mWindow == null) { mWindow = new MyWindow(this); - mLayout.type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; + mLayout.type = mWindowType; mLayout.gravity = Gravity.LEFT|Gravity.TOP; mSession.add(mWindow, mLayout, mVisible ? VISIBLE : GONE, mContentInsets); @@ -356,15 +373,12 @@ public class SurfaceView extends View { mSurfaceLock.lock(); mDrawingStopped = !visible; + final int relayoutResult = mSession.relayout( mWindow, mLayout, mWidth, mHeight, visible ? VISIBLE : GONE, false, mWinFrame, mContentInsets, mVisibleInsets, mSurface); - mContentInsets.scale(mAppScaleInverted); - mVisibleInsets.scale(mAppScaleInverted); - mWinFrame.scale(mAppScaleInverted); - if (localLOGV) Log.i(TAG, "New surface: " + mSurface + ", vis=" + visible + ", frame=" + mWinFrame); mSurfaceFrame.left = 0; @@ -433,24 +447,14 @@ public class SurfaceView extends View { private static class MyWindow extends IWindow.Stub { private final WeakReference<SurfaceView> mSurfaceView; - private final float mAppScale; - private final float mAppScaleInverted; public MyWindow(SurfaceView surfaceView) { mSurfaceView = new WeakReference<SurfaceView>(surfaceView); - mAppScale = surfaceView.getContext().getApplicationScale(); - mAppScaleInverted = 1.0f / mAppScale; } public void resized(int w, int h, Rect coveredInsets, Rect visibleInsets, boolean reportDraw) { SurfaceView surfaceView = mSurfaceView.get(); - float scale = mAppScaleInverted; - w *= scale; - h *= scale; - coveredInsets.scale(scale); - visibleInsets.scale(scale); - if (surfaceView != null) { if (localLOGV) Log.v( "SurfaceView", surfaceView + " got resized: w=" + @@ -613,7 +617,6 @@ public class SurfaceView extends View { Canvas c = null; if (!mDrawingStopped && mWindow != null) { Rect frame = dirty != null ? dirty : mSurfaceFrame; - frame.scale(mAppScale); try { c = mSurface.lockCanvas(frame); } catch (Exception e) { diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java index c708f54..5d89c46 100644 --- a/core/java/android/view/VelocityTracker.java +++ b/core/java/android/view/VelocityTracker.java @@ -165,7 +165,17 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { pastTime[i] = 0; } } - + + /** + * Equivalent to invoking {@link #computeCurrentVelocity(int, float)} with a maximum + * velocity of Float.MAX_VALUE. + * + * @see #computeCurrentVelocity(int, float) + */ + public void computeCurrentVelocity(int units) { + computeCurrentVelocity(units, Float.MAX_VALUE); + } + /** * Compute the current velocity based on the points that have been * collected. Only call this when you actually want to retrieve velocity @@ -175,8 +185,11 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * * @param units The units you would like the velocity in. A value of 1 * provides pixels per millisecond, 1000 provides pixels per second, etc. + * @param maxVelocity The maximum velocity that can be computed by this method. + * This value must be declared in the same unit as the units parameter. This value + * must be positive. */ - public void computeCurrentVelocity(int units) { + public void computeCurrentVelocity(int units, float maxVelocity) { final float[] pastX = mPastX; final float[] pastY = mPastY; final long[] pastTime = mPastTime; @@ -210,8 +223,8 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { if (accumY == 0) accumY = vel; else accumY = (accumY + vel) * .5f; } - mXVelocity = accumX; - mYVelocity = accumY; + mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity) : Math.min(accumX, maxVelocity); + mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity) : Math.min(accumY, maxVelocity); if (localLOGV) Log.v(TAG, "Y velocity=" + mYVelocity +" X velocity=" + mXVelocity + " N=" + N); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 9e709cf..ff8868b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -16,6 +16,9 @@ package android.view; +import com.android.internal.R; +import com.android.internal.view.menu.MenuBuilder; + import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -25,12 +28,12 @@ import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; +import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Region; import android.graphics.Shader; -import android.graphics.Point; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Handler; @@ -42,47 +45,47 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; import android.util.AttributeSet; +import android.util.Config; import android.util.EventLog; import android.util.Log; -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 android.util.Config; +import android.util.Pools; +import android.util.SparseArray; import android.view.ContextMenu.ContextMenuInfo; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityEventSource; +import android.view.accessibility.AccessibilityManager; import android.view.animation.Animation; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.EditorInfo; import android.widget.ScrollBarDrawable; -import com.android.internal.R; -import com.android.internal.view.menu.MenuBuilder; - +import java.lang.ref.SoftReference; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.WeakHashMap; -import java.lang.ref.SoftReference; -import java.lang.reflect.Method; -import java.lang.reflect.InvocationTargetException; /** * <p> * This class represents the basic building block for user interface components. A View * occupies a rectangular area on the screen and is responsible for drawing and * event handling. View is the base class for <em>widgets</em>, which are - * used to create interactive UI components (buttons, text fields, etc.). The + * used to create interactive UI components (buttons, text fields, etc.). The * {@link android.view.ViewGroup} subclass is the base class for <em>layouts</em>, which * are invisible containers that hold other Views (or other ViewGroups) and define * their layout properties. * </p> * * <div class="special"> - * <p>For an introduction to using this class to develop your - * application's user interface, read the Developer Guide documentation on + * <p>For an introduction to using this class to develop your + * application's user interface, read the Developer Guide documentation on * <strong><a href="{@docRoot}guide/topics/ui/index.html">User Interface</a></strong>. Special topics - * include: + * include: * <br/><a href="{@docRoot}guide/topics/ui/declaring-layout.html">Declaring Layout</a> * <br/><a href="{@docRoot}guide/topics/ui/menus.html">Creating Menus</a> * <br/><a href="{@docRoot}guide/topics/ui/layout-objects.html">Common Layout Objects</a> @@ -93,7 +96,7 @@ import java.lang.reflect.InvocationTargetException; * <br/><a href="{@docRoot}guide/topics/ui/how-android-draws.html">How Android Draws Views</a>. * </p> * </div> - * + * * <a name="Using"></a> * <h3>Using Views</h3> * <p> @@ -419,7 +422,7 @@ import java.lang.reflect.InvocationTargetException; * </p> * * <p> - * Note that the framework will not draw views that are not in the invalid region. + * Note that the framework will not draw views that are not in the invalid region. * </p> * * <p> @@ -535,25 +538,52 @@ import java.lang.reflect.InvocationTargetException; * take care of redrawing the appropriate views until the animation completes. * </p> * + * @attr ref android.R.styleable#View_background + * @attr ref android.R.styleable#View_clickable + * @attr ref android.R.styleable#View_contentDescription + * @attr ref android.R.styleable#View_drawingCacheQuality + * @attr ref android.R.styleable#View_duplicateParentState + * @attr ref android.R.styleable#View_id + * @attr ref android.R.styleable#View_fadingEdge + * @attr ref android.R.styleable#View_fadingEdgeLength * @attr ref android.R.styleable#View_fitsSystemWindows + * @attr ref android.R.styleable#View_isScrollContainer + * @attr ref android.R.styleable#View_focusable + * @attr ref android.R.styleable#View_focusableInTouchMode + * @attr ref android.R.styleable#View_hapticFeedbackEnabled + * @attr ref android.R.styleable#View_keepScreenOn + * @attr ref android.R.styleable#View_longClickable + * @attr ref android.R.styleable#View_minHeight + * @attr ref android.R.styleable#View_minWidth * @attr ref android.R.styleable#View_nextFocusDown * @attr ref android.R.styleable#View_nextFocusLeft * @attr ref android.R.styleable#View_nextFocusRight * @attr ref android.R.styleable#View_nextFocusUp + * @attr ref android.R.styleable#View_onClick + * @attr ref android.R.styleable#View_padding + * @attr ref android.R.styleable#View_paddingBottom + * @attr ref android.R.styleable#View_paddingLeft + * @attr ref android.R.styleable#View_paddingRight + * @attr ref android.R.styleable#View_paddingTop + * @attr ref android.R.styleable#View_saveEnabled * @attr ref android.R.styleable#View_scrollX * @attr ref android.R.styleable#View_scrollY - * @attr ref android.R.styleable#View_scrollbarTrackHorizontal - * @attr ref android.R.styleable#View_scrollbarThumbHorizontal * @attr ref android.R.styleable#View_scrollbarSize + * @attr ref android.R.styleable#View_scrollbarStyle * @attr ref android.R.styleable#View_scrollbars + * @attr ref android.R.styleable#View_scrollbarTrackHorizontal + * @attr ref android.R.styleable#View_scrollbarThumbHorizontal * @attr ref android.R.styleable#View_scrollbarThumbVertical * @attr ref android.R.styleable#View_scrollbarTrackVertical * @attr ref android.R.styleable#View_scrollbarAlwaysDrawHorizontalTrack * @attr ref android.R.styleable#View_scrollbarAlwaysDrawVerticalTrack + * @attr ref android.R.styleable#View_soundEffectsEnabled + * @attr ref android.R.styleable#View_tag + * @attr ref android.R.styleable#View_visibility * * @see android.view.ViewGroup */ -public class View implements Drawable.Callback, KeyEvent.Callback { +public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { private static final boolean DBG = false; /** @@ -851,6 +881,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public static final int HAPTIC_FEEDBACK_ENABLED = 0x10000000; /** + * View flag indicating whether {@link #addFocusables(ArrayList, int, int)} + * should add all focusable Views regardless if they are focusable in touch mode. + */ + public static final int FOCUSABLES_ALL = 0x00000000; + + /** + * View flag indicating whether {@link #addFocusables(ArrayList, int, int)} + * should add only Views focusable in touch mode. + */ + public static final int FOCUSABLES_TOUCH_MODE = 0x00000001; + + /** * Use with {@link #focusSearch}. Move focus to the previous selectable * item. */ @@ -1428,6 +1470,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback { static final int DIRTY_MASK = 0x00600000; /** + * Indicates whether the background is opaque. + * + * @hide + */ + static final int OPAQUE_BACKGROUND = 0x00800000; + + /** + * Indicates whether the scrollbars are opaque. + * + * @hide + */ + static final int OPAQUE_SCROLLBARS = 0x01000000; + + /** + * Indicates whether the view is opaque. + * + * @hide + */ + static final int OPAQUE_MASK = 0x01800000; + + /** * The parent this view is attached to. * {@hide} * @@ -1449,7 +1512,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { @ViewDebug.FlagToString(mask = LAYOUT_REQUIRED, equals = LAYOUT_REQUIRED, name = "LAYOUT_REQUIRED"), @ViewDebug.FlagToString(mask = DRAWING_CACHE_VALID, equals = DRAWING_CACHE_VALID, - name = "DRAWING_CACHE_VALID", outputIf = false), + name = "DRAWING_CACHE_INVALID", outputIf = false), @ViewDebug.FlagToString(mask = DRAWN, equals = DRAWN, name = "DRAWN", outputIf = true), @ViewDebug.FlagToString(mask = DRAWN, equals = DRAWN, name = "NOT_DRAWN", outputIf = false), @ViewDebug.FlagToString(mask = DIRTY_MASK, equals = DIRTY_OPAQUE, name = "DIRTY_OPAQUE"), @@ -1551,6 +1614,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { protected int mPaddingBottom; /** + * Briefly describes the view and is primarily used for accessibility support. + */ + private CharSequence mContentDescription; + + /** * Cache the paddingRight set by the user to append to the scrollbar's size. */ @ViewDebug.ExportedProperty @@ -1622,6 +1690,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { private int[] mDrawableState = null; private SoftReference<Bitmap> mDrawingCache; + private SoftReference<Bitmap> mUnscaledDrawingCache; /** * When this view has focus and the next focus is {@link #FOCUS_LEFT}, @@ -1701,7 +1770,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { 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; ++sInstanceCount; } @@ -1762,7 +1831,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { int viewFlagMasks = 0; boolean setScrollContainer = false; - + int x = 0; int y = 0; @@ -1858,16 +1927,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback { viewFlagMasks |= DRAWING_CACHE_QUALITY_MASK; } break; + case com.android.internal.R.styleable.View_contentDescription: + mContentDescription = a.getString(attr); + break; case com.android.internal.R.styleable.View_soundEffectsEnabled: if (!a.getBoolean(attr, true)) { viewFlagValues &= ~SOUND_EFFECTS_ENABLED; viewFlagMasks |= SOUND_EFFECTS_ENABLED; } + break; case com.android.internal.R.styleable.View_hapticFeedbackEnabled: if (!a.getBoolean(attr, true)) { viewFlagValues &= ~HAPTIC_FEEDBACK_ENABLED; viewFlagMasks |= HAPTIC_FEEDBACK_ENABLED; } + break; case R.styleable.View_scrollbars: final int scrollbars = a.getInt(attr, SCROLLBARS_NONE); if (scrollbars != SCROLLBARS_NONE) { @@ -1922,6 +1996,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mMinHeight = a.getDimensionPixelSize(attr, 0); break; case R.styleable.View_onClick: + if (context.isRestricted()) { + throw new IllegalStateException("The android:onClick attribute cannot " + + "be used within a restricted context"); + } + final String handlerName = a.getString(attr); if (handlerName != null) { setOnClickListener(new OnClickListener() { @@ -1990,7 +2069,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (!setScrollContainer && (viewFlagValues&SCROLLBARS_VERTICAL) != 0) { setScrollContainer(true); } - + + computeOpaqueFlags(); + a.recycle(); } @@ -2255,6 +2336,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * otherwise is returned. */ public boolean performClick() { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); + if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); @@ -2272,6 +2355,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * otherwise is returned. */ public boolean performLongClick() { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); + boolean handled = false; if (mOnLongClickListener != null) { handled = mOnLongClickListener.onLongClick(View.this); @@ -2387,7 +2472,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (!(parent instanceof View)) { break; } - + child = (View) parent; parent = child.getParent(); } @@ -2479,7 +2564,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * and previouslyFocusedRect provide insight into where the focus is coming from. * When overriding, be sure to call up through to the super class so that * the standard focus handling will occur. - * + * * @param gainFocus True if the View has focus; false otherwise. * @param direction The direction focus has moved when requestFocus() * is called to give this view focus. Values are @@ -2492,6 +2577,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * from (in addition to direction). Will be <code>null</code> otherwise. */ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { + if (gainFocus) { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + } + InputMethodManager imm = InputMethodManager.peekInstance(); if (!gainFocus) { if (isPressed()) { @@ -2506,7 +2595,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { && mAttachInfo.mHasWindowFocus) { imm.focusIn(this); } - + invalidate(); if (mOnFocusChangeListener != null) { mOnFocusChangeListener.onFocusChange(this, gainFocus); @@ -2514,6 +2603,79 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } /** + * {@inheritDoc} + */ + public void sendAccessibilityEvent(int eventType) { + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + sendAccessibilityEventUnchecked(AccessibilityEvent.obtain(eventType)); + } + } + + /** + * {@inheritDoc} + */ + public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { + event.setClassName(getClass().getName()); + event.setPackageName(getContext().getPackageName()); + event.setEnabled(isEnabled()); + event.setContentDescription(mContentDescription); + + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && mAttachInfo != null) { + ArrayList<View> focusablesTempList = mAttachInfo.mFocusablesTempList; + getRootView().addFocusables(focusablesTempList, View.FOCUS_FORWARD, FOCUSABLES_ALL); + event.setItemCount(focusablesTempList.size()); + event.setCurrentItemIndex(focusablesTempList.indexOf(this)); + focusablesTempList.clear(); + } + + dispatchPopulateAccessibilityEvent(event); + + AccessibilityManager.getInstance(mContext).sendAccessibilityEvent(event); + } + + /** + * Dispatches an {@link AccessibilityEvent} to the {@link View} children + * to be populated. + * + * @param event The event. + * + * @return True if the event population was completed. + */ + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + return false; + } + + /** + * Gets the {@link View} description. It briefly describes the view and is + * primarily used for accessibility support. Set this property to enable + * better accessibility support for your application. This is especially + * true for views that do not have textual representation (For example, + * ImageButton). + * + * @return The content descriptiopn. + * + * @attr ref android.R.styleable#View_contentDescription + */ + public CharSequence getContentDescription() { + return mContentDescription; + } + + /** + * Sets the {@link View} description. It briefly describes the view and is + * primarily used for accessibility support. Set this property to enable + * better accessibility support for your application. This is especially + * true for views that do not have textual representation (For example, + * ImageButton). + * + * @param contentDescription The content description. + * + * @attr ref android.R.styleable#View_contentDescription + */ + public void setContentDescription(CharSequence contentDescription) { + mContentDescription = contentDescription; + } + + /** * Invoked whenever this view loses focus, either by losing window focus or by losing * focus within its window. This method can be used to clear any state tied to the * focus. For instance, if a button is held pressed with the trackball and the window @@ -2522,7 +2684,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * Subclasses of View overriding this method should always call super.onFocusLost(). * * @see #onFocusChanged(boolean, int, android.graphics.Rect) - * @see #onWindowFocusChanged(boolean) + * @see #onWindowFocusChanged(boolean) * * @hide pending API council approval */ @@ -3222,11 +3384,37 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @param direction The direction of the focus */ public void addFocusables(ArrayList<View> views, int direction) { - if (!isFocusable()) return; + addFocusables(views, direction, FOCUSABLES_TOUCH_MODE); + } - if (isInTouchMode() && !isFocusableInTouchMode()) return; + /** + * Adds any focusable views that are descendants of this view (possibly + * including this view if it is focusable itself) to views. This method + * adds all focusable views regardless if we are in touch mode or + * only views focusable in touch mode if we are in touch mode depending on + * the focusable mode paramater. + * + * @param views Focusable views found so far or null if all we are interested is + * the number of focusables. + * @param direction The direction of the focus. + * @param focusableMode The type of focusables to be added. + * + * @see #FOCUSABLES_ALL + * @see #FOCUSABLES_TOUCH_MODE + */ + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { + if (!isFocusable()) { + return; + } - views.add(this); + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && + isInTouchMode() && !isFocusableInTouchMode()) { + return; + } + + if (views != null) { + views.add(this); + } } /** @@ -3398,14 +3586,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback { */ public void onStartTemporaryDetach() { } - + /** * Called after {@link #onStartTemporaryDetach} when the container is done * changing the view. */ public void onFinishTemporaryDetach() { } - + /** * capture information of this view for later analysis: developement only * check dynamic switch to make sure we only dump view @@ -3790,25 +3978,25 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * a call on that method would return a non-null InputConnection, and * they are really a first-class editor that the user would normally * start typing on when the go into a window containing your view. - * + * * <p>The default implementation always returns false. This does * <em>not</em> mean that its {@link #onCreateInputConnection(EditorInfo)} * will not be called or the user can not otherwise perform edits on your * view; it is just a hint to the system that this is not the primary * purpose of this view. - * + * * @return Returns true if this view is a text editor, else false. */ public boolean onCheckIsTextEditor() { return false; } - + /** * Create a new InputConnection for an InputMethod to interact * with the view. The default implementation returns null, since it doesn't * support input methods. You can override this to implement such support. * This is only needed for views that take focus and text input. - * + * * <p>When implementing this, you probably also want to implement * {@link #onCheckIsTextEditor()} to indicate you will return a * non-null InputConnection. @@ -3832,7 +4020,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public boolean checkInputConnectionProxy(View view) { return false; } - + /** * Show the context menu for this view. It is not safe to hold on to the * menu after returning from this method. @@ -4563,14 +4751,42 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * whether an instance is opaque. Opaque Views are treated in a special way by * the View hierarchy, possibly allowing it to perform optimizations during * invalidate/draw passes. - * + * * @return True if this View is guaranteed to be fully opaque, false otherwise. * * @hide Pending API council approval */ @ViewDebug.ExportedProperty public boolean isOpaque() { - return mBGDrawable != null && mBGDrawable.getOpacity() == PixelFormat.OPAQUE; + return (mPrivateFlags & OPAQUE_MASK) == OPAQUE_MASK; + } + + private void computeOpaqueFlags() { + // Opaque if: + // - Has a background + // - Background is opaque + // - Doesn't have scrollbars or scrollbars are inside overlay + + if (mBGDrawable != null && mBGDrawable.getOpacity() == PixelFormat.OPAQUE) { + mPrivateFlags |= OPAQUE_BACKGROUND; + } else { + mPrivateFlags &= ~OPAQUE_BACKGROUND; + } + + final int flags = mViewFlags; + if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) || + (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY) { + mPrivateFlags |= OPAQUE_SCROLLBARS; + } else { + mPrivateFlags &= ~OPAQUE_SCROLLBARS; + } + } + + /** + * @hide + */ + protected boolean hasOpaqueScrollbars() { + return (mPrivateFlags & OPAQUE_SCROLLBARS) == OPAQUE_SCROLLBARS; } /** @@ -4897,6 +5113,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled) { if (isHorizontalScrollBarEnabled() != horizontalScrollBarEnabled) { mViewFlags ^= SCROLLBARS_HORIZONTAL; + computeOpaqueFlags(); recomputePadding(); } } @@ -4926,6 +5143,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) { if (isVerticalScrollBarEnabled() != verticalScrollBarEnabled) { mViewFlags ^= SCROLLBARS_VERTICAL; + computeOpaqueFlags(); recomputePadding(); } } @@ -4954,6 +5172,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { public void setScrollBarStyle(int style) { if (style != (mViewFlags & SCROLLBARS_STYLE_MASK)) { mViewFlags = (mViewFlags & ~SCROLLBARS_STYLE_MASK) | (style & SCROLLBARS_STYLE_MASK); + computeOpaqueFlags(); recomputePadding(); } } @@ -5132,9 +5351,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } } } - + /** - * Override this if the vertical scrollbar needs to be hidden in a subclass, like when + * Override this if the vertical scrollbar needs to be hidden in a subclass, like when * FastScroller is visible. * @return whether to temporarily hide the vertical scrollbar * @hide @@ -5570,28 +5789,52 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } /** + * <p>Calling this method is equivalent to calling <code>getDrawingCache(false)</code>.</p> + * + * @return A non-scaled bitmap representing this view or null if cache is disabled. + * + * @see #getDrawingCache(boolean) + */ + public Bitmap getDrawingCache() { + return getDrawingCache(false); + } + + /** * <p>Returns the bitmap in which this view drawing is cached. The returned bitmap * is null when caching is disabled. If caching is enabled and the cache is not ready, * this method will create it. Calling {@link #draw(android.graphics.Canvas)} will not * draw from the cache when the cache is enabled. To benefit from the cache, you must * request the drawing cache by calling this method and draw it on screen if the * returned bitmap is not null.</p> + * + * <p>Note about auto scaling in compatibility mode: When auto scaling is not enabled, + * this method will create a bitmap of the same size as this view. Because this bitmap + * will be drawn scaled by the parent ViewGroup, the result on screen might show + * scaling artifacts. To avoid such artifacts, you should call this method by setting + * the auto scaling to true. Doing so, however, will generate a bitmap of a different + * size than the view. This implies that your application must be able to handle this + * size.</p> + * + * @param autoScale Indicates whether the generated bitmap should be scaled based on + * the current density of the screen when the application is in compatibility + * mode. * - * @return a bitmap representing this view or null if cache is disabled - * + * @return A bitmap representing this view or null if cache is disabled. + * * @see #setDrawingCacheEnabled(boolean) * @see #isDrawingCacheEnabled() - * @see #buildDrawingCache() + * @see #buildDrawingCache(boolean) * @see #destroyDrawingCache() */ - public Bitmap getDrawingCache() { + public Bitmap getDrawingCache(boolean autoScale) { if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) { return null; } if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) { - buildDrawingCache(); + buildDrawingCache(autoScale); } - return mDrawingCache == null ? null : mDrawingCache.get(); + return autoScale ? (mDrawingCache == null ? null : mDrawingCache.get()) : + (mUnscaledDrawingCache == null ? null : mUnscaledDrawingCache.get()); } /** @@ -5610,6 +5853,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { if (bitmap != null) bitmap.recycle(); mDrawingCache = null; } + if (mUnscaledDrawingCache != null) { + final Bitmap bitmap = mUnscaledDrawingCache.get(); + if (bitmap != null) bitmap.recycle(); + mUnscaledDrawingCache = null; + } } /** @@ -5637,18 +5885,36 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } /** + * <p>Calling this method is equivalent to calling <code>buildDrawingCache(false)</code>.</p> + * + * @see #buildDrawingCache(boolean) + */ + public void buildDrawingCache() { + buildDrawingCache(false); + } + + /** * <p>Forces the drawing cache to be built if the drawing cache is invalid.</p> * * <p>If you call {@link #buildDrawingCache()} manually without calling * {@link #setDrawingCacheEnabled(boolean) setDrawingCacheEnabled(true)}, you * should cleanup the cache by calling {@link #destroyDrawingCache()} afterwards.</p> + * + * <p>Note about auto scaling in compatibility mode: When auto scaling is not enabled, + * this method will create a bitmap of the same size as this view. Because this bitmap + * will be drawn scaled by the parent ViewGroup, the result on screen might show + * scaling artifacts. To avoid such artifacts, you should call this method by setting + * the auto scaling to true. Doing so, however, will generate a bitmap of a different + * size than the view. This implies that your application must be able to handle this + * size.</p> * * @see #getDrawingCache() * @see #destroyDrawingCache() */ - public void buildDrawingCache() { - if ((mPrivateFlags & DRAWING_CACHE_VALID) == 0 || mDrawingCache == null || - mDrawingCache.get() == null) { + public void buildDrawingCache(boolean autoScale) { + if ((mPrivateFlags & DRAWING_CACHE_VALID) == 0 || (autoScale ? + (mDrawingCache == null || mDrawingCache.get() == null) : + (mUnscaledDrawingCache == null || mUnscaledDrawingCache.get() == null))) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.BUILD_CACHE); @@ -5657,8 +5923,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback { EventLog.writeEvent(60002, hashCode()); } - final int width = mRight - mLeft; - final int height = mBottom - mTop; + int width = mRight - mLeft; + int height = mBottom - mTop; + + final AttachInfo attachInfo = mAttachInfo; + final boolean scalingRequired = attachInfo != null && attachInfo.mScalingRequired; + + if (autoScale && scalingRequired) { + width = (int) ((width * attachInfo.mApplicationScale) + 0.5f); + height = (int) ((height * attachInfo.mApplicationScale) + 0.5f); + } final int drawingCacheBackgroundColor = mDrawingCacheBackgroundColor; final boolean opaque = drawingCacheBackgroundColor != 0 || @@ -5672,7 +5946,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } boolean clear = true; - Bitmap bitmap = mDrawingCache == null ? null : mDrawingCache.get(); + Bitmap bitmap = autoScale ? (mDrawingCache == null ? null : mDrawingCache.get()) : + (mUnscaledDrawingCache == null ? null : mUnscaledDrawingCache.get()); if (bitmap == null || bitmap.getWidth() != width || bitmap.getHeight() != height) { @@ -5701,12 +5976,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback { try { bitmap = Bitmap.createBitmap(width, height, quality); - mDrawingCache = new SoftReference<Bitmap>(bitmap); + if (autoScale) { + mDrawingCache = new SoftReference<Bitmap>(bitmap); + } else { + mUnscaledDrawingCache = new SoftReference<Bitmap>(bitmap); + } } catch (OutOfMemoryError e) { // If there is not enough memory to create the bitmap cache, just // ignore the issue as bitmap caches are not required to draw the // view hierarchy - mDrawingCache = null; + if (autoScale) { + mDrawingCache = null; + } else { + mUnscaledDrawingCache = null; + } return; } @@ -5714,7 +5997,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback { } Canvas canvas; - final AttachInfo attachInfo = mAttachInfo; if (attachInfo != null) { canvas = attachInfo.mCanvas; if (canvas == null) { @@ -5737,15 +6019,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback { computeScroll(); final int restoreCount = canvas.save(); + + if (autoScale && scalingRequired) { + final float scale = attachInfo.mApplicationScale; + canvas.scale(scale, scale); + } + canvas.translate(-mScrollX, -mScrollY); - mPrivateFlags = (mPrivateFlags & ~DIRTY_MASK) | DRAWN; + mPrivateFlags |= DRAWN; // Fast path for layouts with no backgrounds if ((mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW); } + mPrivateFlags &= ~DIRTY_MASK; dispatchDraw(canvas); } else { draw(canvas); @@ -5792,7 +6081,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { canvas = new Canvas(bitmap); } - if ((backgroundColor&0xff000000) != 0) { + if ((backgroundColor & 0xff000000) != 0) { bitmap.eraseColor(backgroundColor); } @@ -5800,6 +6089,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { final int restoreCount = canvas.save(); canvas.translate(-mScrollX, -mScrollY); + // Temporarily remove the dirty mask + int flags = mPrivateFlags; + mPrivateFlags &= ~DIRTY_MASK; + // Fast path for layouts with no backgrounds if ((mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) { dispatchDraw(canvas); @@ -5807,13 +6100,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback { draw(canvas); } + mPrivateFlags = flags; + canvas.restoreToCount(restoreCount); if (attachInfo != null) { // Restore the cached Canvas for our siblings attachInfo.mCanvas = canvas; } - + return bitmap; } @@ -5927,8 +6222,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW); } - final boolean dirtyOpaque = (mPrivateFlags & DIRTY_MASK) == DIRTY_OPAQUE; - mPrivateFlags = (mPrivateFlags & ~DIRTY_MASK) | DRAWN; + final int privateFlags = mPrivateFlags; + final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE && + (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); + mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN; /* * Draw traversal performs several drawing steps which must be executed @@ -6306,7 +6603,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { boolean changed = false; if (DBG) { - System.out.println(this + " View.setFrame(" + left + "," + top + "," + Log.d("View", this + " View.setFrame(" + left + "," + top + "," + right + "," + bottom + ")"); } @@ -6709,6 +7006,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback { requestLayout = true; } + computeOpaqueFlags(); + if (requestLayout) { requestLayout(); } @@ -6749,7 +7048,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { mUserPaddingBottom = bottom; final int viewFlags = mViewFlags; - + // Common case is there are no scroll bars. if ((viewFlags & (SCROLLBARS_VERTICAL|SCROLLBARS_HORIZONTAL)) != 0) { // TODO: Deal with RTL languages to adjust left padding instead of right. @@ -6762,7 +7061,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { ? 0 : getHorizontalScrollbarHeight(); } } - + if (mPaddingLeft != left) { changed = true; mPaddingLeft = left; @@ -6899,7 +7198,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { return v; } } - + View parent = this; while (parent.mParent != null && parent.mParent instanceof View) { @@ -6920,8 +7219,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback { getLocationInWindow(location); final AttachInfo info = mAttachInfo; - location[0] += info.mWindowLeft; - location[1] += info.mWindowTop; + if (info != null) { + location[0] += info.mWindowLeft; + location[1] += info.mWindowTop; + } } /** @@ -6947,7 +7248,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { location[1] += view.mTop - view.mScrollY; viewParent = view.mParent; } - + if (viewParent instanceof ViewRoot) { // *cough* final ViewRoot vr = (ViewRoot)viewParent; @@ -7098,7 +7399,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * @return the Object stored in this view as a tag * * @see #setTag(int, Object) - * @see #getTag() + * @see #getTag() */ public Object getTag(int key) { SparseArray<Object> tags = null; @@ -7154,7 +7455,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { + "resource id."); } - setTagInternal(this, key, tag); + setTagInternal(this, key, tag); } private static void setTagInternal(View view, int key, Object tag) { @@ -7189,7 +7490,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * Method that subclasses should implement to check their consistency. The type of * consistency check is indicated by the bit field passed as a parameter. - * + * * @param consistency The type of consistency. See ViewDebug for more information. * * @throws IllegalStateException if the view is in an inconsistent state. @@ -7744,7 +8045,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * BZZZTT!!1! - * + * * <p>Provide haptic feedback to the user for this view. * * <p>The framework will provide haptic feedback for some built in actions, @@ -7763,7 +8064,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback { /** * BZZZTT!!1! - * + * * <p>Like {@link #performHapticFeedback(int)}, with additional options. * * @param feedbackConstant One of the constants defined in @@ -8158,7 +8459,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * window. */ static class AttachInfo { - interface Callbacks { void playSoundEffect(int effectId); boolean performHapticFeedback(int effectId, boolean always); @@ -8227,11 +8527,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * The top view of the hierarchy. */ View mRootView; - + IBinder mPanelParentWindowToken; Surface mSurface; /** + * Scale factor used by the compatibility mode + */ + float mApplicationScale; + + /** + * Indicates whether the application is in compatibility mode + */ + boolean mScalingRequired; + + /** * Left position of this view's window */ int mWindowLeft; @@ -8288,6 +8598,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback { long mDrawingTime; /** + * Indicates whether or not ignoring the DIRTY_MASK flags. + */ + boolean mIgnoreDirtyState; + + /** * Indicates whether the view's window is currently in touch mode. */ boolean mInTouchMode; @@ -8365,7 +8680,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback { * calling up the hierarchy. */ final Rect mTmpInvalRect = new Rect(); - + + /** + * Temporary list for use in collecting focusable descendents of a view. + */ + final ArrayList<View> mFocusablesTempList = new ArrayList<View>(24); + /** * Creates a new set of attachment information with the specified * events handler and thread. @@ -8408,18 +8728,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback { // use use a height of 1, and then wack the matrix each time we // actually use it. shader = new LinearGradient(0, 0, 0, 1, 0xFF000000, 0, Shader.TileMode.CLAMP); - + paint.setShader(shader); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); } - + public void setFadeColor(int color) { if (color != 0 && color != mLastColor) { mLastColor = color; color |= 0xFF000000; - + shader = new LinearGradient(0, 0, 0, 1, color, 0, Shader.TileMode.CLAMP); - + paint.setShader(shader); // Restore the default transfer mode (src_over) paint.setXfermode(null); diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 8e1524b..0e36ec2 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -106,6 +106,11 @@ public class ViewConfiguration { * Minimum velocity to initiate a fling, as measured in pixels per second */ private static final int MINIMUM_FLING_VELOCITY = 50; + + /** + * Maximum velocity to initiate a fling, as measured in pixels per second + */ + private static final int MAXIMUM_FLING_VELOCITY = 4000; /** * The maximum size of View's drawing cache, expressed in bytes. This size @@ -122,6 +127,7 @@ public class ViewConfiguration { private final int mEdgeSlop; private final int mFadingEdgeLength; private final int mMinimumFlingVelocity; + private final int mMaximumFlingVelocity; private final int mScrollbarSize; private final int mTouchSlop; private final int mDoubleTapSlop; @@ -139,6 +145,7 @@ public class ViewConfiguration { mEdgeSlop = EDGE_SLOP; mFadingEdgeLength = FADING_EDGE_LENGTH; mMinimumFlingVelocity = MINIMUM_FLING_VELOCITY; + mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY; mScrollbarSize = SCROLL_BAR_SIZE; mTouchSlop = TOUCH_SLOP; mDoubleTapSlop = DOUBLE_TAP_SLOP; @@ -164,6 +171,7 @@ public class ViewConfiguration { mEdgeSlop = (int) (density * EDGE_SLOP + 0.5f); mFadingEdgeLength = (int) (density * FADING_EDGE_LENGTH + 0.5f); mMinimumFlingVelocity = (int) (density * MINIMUM_FLING_VELOCITY + 0.5f); + mMaximumFlingVelocity = (int) (density * MAXIMUM_FLING_VELOCITY + 0.5f); mScrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f); mTouchSlop = (int) (density * TOUCH_SLOP + 0.5f); mDoubleTapSlop = (int) (density * DOUBLE_TAP_SLOP + 0.5f); @@ -367,6 +375,23 @@ public class ViewConfiguration { } /** + * @return Maximum velocity to initiate a fling, as measured in pixels per second. + * + * @deprecated Use {@link #getScaledMaximumFlingVelocity()} instead. + */ + @Deprecated + public static int getMaximumFlingVelocity() { + return MAXIMUM_FLING_VELOCITY; + } + + /** + * @return Maximum velocity to initiate a fling, as measured in pixels per second. + */ + public int getScaledMaximumFlingVelocity() { + return mMaximumFlingVelocity; + } + + /** * The maximum drawing cache size expressed in bytes. * * @return the maximum size of View's drawing cache expressed in bytes diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 74a248f..46aea02 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -87,17 +87,17 @@ public class ViewDebug { * check that this value is set to true as not to affect performance. */ public static final boolean TRACE_RECYCLER = 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"; /** @@ -216,7 +216,7 @@ public class ViewDebug { * <pre> * * A specified String is output when the following is true: - * + * * @return An array of int to String mappings */ FlagToString[] flagMapping() default { }; @@ -228,7 +228,7 @@ public class ViewDebug { * * @return true if the properties of this property should be dumped * - * @see #prefix() + * @see #prefix() */ boolean deepExport() default false; @@ -313,15 +313,15 @@ public class ViewDebug { @Retention(RetentionPolicy.RUNTIME) public @interface CapturedViewProperty { /** - * When retrieveReturn is true, we need to retrieve second level methods + * When retrieveReturn is true, we need to retrieve second level methods * e.g., we need myView.getFirstLevelMethod().getSecondLevelMethod() - * we will set retrieveReturn = true on the annotation of + * we will set retrieveReturn = true on the annotation of * myView.getFirstLevelMethod() - * @return true if we need the second level methods + * @return true if we need the second level methods */ - boolean retrieveReturn() default false; + boolean retrieveReturn() default false; } - + private static HashMap<Class<?>, Method[]> mCapturedViewMethodsForClasses = null; private static HashMap<Class<?>, Field[]> mCapturedViewFieldsForClasses = null; @@ -401,7 +401,7 @@ public class ViewDebug { */ public static long getViewRootInstanceCount() { return ViewRoot.getInstanceCount(); - } + } /** * Outputs a trace to the currently opened recycler traces. The trace records the type of @@ -624,7 +624,7 @@ public class ViewDebug { * * This method will return immediately if TRACE_HIERARCHY is false. * - * @see #startHierarchyTracing(String, View) + * @see #startHierarchyTracing(String, View) * @see #trace(View, android.view.ViewDebug.HierarchyTraceType) */ public static void stopHierarchyTracing() { @@ -671,7 +671,7 @@ public class ViewDebug { sHierarhcyRoot = null; } - + static void dispatchCommand(View view, String command, String parameters, OutputStream clientStream) throws IOException { @@ -1039,10 +1039,10 @@ public class ViewDebug { final ArrayList<Method> foundMethods = new ArrayList<Method>(); methods = klass.getDeclaredMethods(); - + int count = methods.length; for (int i = 0; i < count; i++) { - final Method method = methods[i]; + final Method method = methods[i]; if (method.getParameterTypes().length == 0 && method.isAnnotationPresent(ExportedProperty.class) && method.getReturnType() != Void.class) { @@ -1075,7 +1075,7 @@ public class ViewDebug { klass = klass.getSuperclass(); } while (klass != Object.class); } - + private static void exportMethods(Context context, Object view, BufferedWriter out, Class<?> klass, String prefix) throws IOException { @@ -1235,10 +1235,11 @@ public class ViewDebug { for (int j = 0; j < count; j++) { final FlagToString flagMapping = mapping[j]; final boolean ifTrue = flagMapping.outputIf(); - final boolean test = (intValue & flagMapping.mask()) == flagMapping.equals(); + final int maskResult = intValue & flagMapping.mask(); + final boolean test = maskResult == flagMapping.equals(); if ((test && ifTrue) || (!test && !ifTrue)) { final String name = flagMapping.name(); - final String value = ifTrue ? "true" : "false"; + final String value = "0x" + Integer.toHexString(maskResult); writeEntry(out, prefix, name, "", value); } } @@ -1259,7 +1260,7 @@ public class ViewDebug { for (int j = 0; j < valuesCount; j++) { String name; - String value; + String value = null; final int intValue = array[j]; @@ -1275,7 +1276,6 @@ public class ViewDebug { } } - value = String.valueOf(intValue); if (hasMapping) { int mappingCount = mapping.length; for (int k = 0; k < mappingCount; k++) { @@ -1288,7 +1288,9 @@ public class ViewDebug { } if (resolveId) { - value = (String) resolveId(context, intValue); + if (value == null) value = (String) resolveId(context, intValue); + } else { + value = String.valueOf(intValue); } writeEntry(out, prefix, name, suffix, value); @@ -1396,10 +1398,10 @@ public class ViewDebug { final ArrayList<Method> foundMethods = new ArrayList<Method>(); methods = klass.getMethods(); - + int count = methods.length; for (int i = 0; i < count; i++) { - final Method method = methods[i]; + final Method method = methods[i]; if (method.getParameterTypes().length == 0 && method.isAnnotationPresent(CapturedViewProperty.class) && method.getReturnType() != Void.class) { @@ -1413,14 +1415,14 @@ public class ViewDebug { return methods; } - - private static String capturedViewExportMethods(Object obj, Class<?> klass, + + private static String capturedViewExportMethods(Object obj, Class<?> klass, String prefix) { if (obj == null) { return "null"; } - + StringBuilder sb = new StringBuilder(); final Method[] methods = capturedViewGetPropertyMethods(klass); @@ -1430,41 +1432,41 @@ public class ViewDebug { try { Object methodValue = method.invoke(obj, (Object[]) null); final Class<?> returnType = method.getReturnType(); - + CapturedViewProperty property = method.getAnnotation(CapturedViewProperty.class); if (property.retrieveReturn()) { //we are interested in the second level data only sb.append(capturedViewExportMethods(methodValue, returnType, method.getName() + "#")); - } else { + } else { sb.append(prefix); sb.append(method.getName()); sb.append("()="); - + if (methodValue != null) { - final String value = methodValue.toString().replace("\n", "\\n"); - sb.append(value); + final String value = methodValue.toString().replace("\n", "\\n"); + sb.append(value); } else { sb.append("null"); } sb.append("; "); } } catch (IllegalAccessException e) { - //Exception IllegalAccess, it is OK here + //Exception IllegalAccess, it is OK here //we simply ignore this method } catch (InvocationTargetException e) { - //Exception InvocationTarget, it is OK here + //Exception InvocationTarget, it is OK here //we simply ignore this method - } - } + } + } return sb.toString(); } private static String capturedViewExportFields(Object obj, Class<?> klass, String prefix) { - + if (obj == null) { return "null"; } - + StringBuilder sb = new StringBuilder(); final Field[] fields = capturedViewGetPropertyFields(klass); @@ -1486,25 +1488,25 @@ public class ViewDebug { } sb.append(' '); } catch (IllegalAccessException e) { - //Exception IllegalAccess, it is OK here + //Exception IllegalAccess, it is OK here //we simply ignore this field } } return sb.toString(); } - + /** - * Dump view info for id based instrument test generation + * Dump view info for id based instrument test generation * (and possibly further data analysis). The results are dumped - * to the log. + * to the log. * @param tag for log * @param view for dump */ - public static void dumpCapturedView(String tag, Object view) { + public static void dumpCapturedView(String tag, Object view) { Class<?> klass = view.getClass(); StringBuilder sb = new StringBuilder(klass.getName() + ": "); sb.append(capturedViewExportFields(view, klass, "")); - sb.append(capturedViewExportMethods(view, klass, "")); - Log.d(tag, sb.toString()); + sb.append(capturedViewExportMethods(view, klass, "")); + Log.d(tag, sb.toString()); } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 26fe776..f7b7f02 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -24,15 +24,16 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; -import android.graphics.Region; import android.graphics.RectF; +import android.graphics.Region; import android.os.Parcelable; import android.os.SystemClock; import android.util.AttributeSet; +import android.util.Config; import android.util.EventLog; import android.util.Log; import android.util.SparseArray; -import android.util.Config; +import android.view.accessibility.AccessibilityEvent; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.animation.LayoutAnimationController; @@ -52,6 +53,15 @@ import java.util.ArrayList; * <p> * Also see {@link LayoutParams} for layout attributes. * </p> + * + * @attr ref android.R.styleable#ViewGroup_clipChildren + * @attr ref android.R.styleable#ViewGroup_clipToPadding + * @attr ref android.R.styleable#ViewGroup_layoutAnimation + * @attr ref android.R.styleable#ViewGroup_animationCache + * @attr ref android.R.styleable#ViewGroup_persistentDrawingCache + * @attr ref android.R.styleable#ViewGroup_alwaysDrawnWithCache + * @attr ref android.R.styleable#ViewGroup_addStatesFromChildren + * @attr ref android.R.styleable#ViewGroup_descendantFocusability */ public abstract class ViewGroup extends View implements ViewParent, ViewManager { private static final boolean DBG = false; @@ -89,7 +99,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Internal flags. - * + * * This field should be made private, so it is hidden from the SDK. * {@hide} */ @@ -142,7 +152,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * to get the index of the child to draw for that iteration. */ protected static final int FLAG_USE_CHILD_DRAWING_ORDER = 0x400; - + /** * When set, this ViewGroup supports static transformations on children; this causes * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} to be @@ -151,7 +161,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * Any subclass overriding * {@link #getChildStaticTransformation(View, android.view.animation.Transformation)} should * set this flags in {@link #mGroupFlags}. - * + * * {@hide} */ protected static final int FLAG_SUPPORT_STATIC_TRANSFORMATIONS = 0x800; @@ -212,7 +222,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * When set, this ViewGroup should not intercept touch events. */ private static final int FLAG_DISALLOW_INTERCEPT = 0x80000; - + /** * Indicates which types of drawing caches are to be kept in memory. * This field should be made private, so it is hidden from the SDK. @@ -601,6 +611,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @Override public void addFocusables(ArrayList<View> views, int direction) { + addFocusables(views, direction, FOCUSABLES_TOUCH_MODE); + } + + /** + * {@inheritDoc} + */ + @Override + public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { final int focusableCount = views.size(); final int descendantFocusability = getDescendantFocusability(); @@ -612,7 +630,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { - child.addFocusables(views, direction); + child.addFocusables(views, direction, focusableMode); } } } @@ -625,7 +643,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager descendantFocusability != FOCUS_AFTER_DESCENDANTS || // No focusable descendants (focusableCount == views.size())) { - super.addFocusables(views, direction); + super.addFocusables(views, direction, focusableMode); } } @@ -680,7 +698,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager ViewParent parent = mParent; if (parent != null) parent.recomputeViewAttributes(this); } - + @Override void dispatchCollectViewAttributes(int visibility) { visibility |= mViewFlags&VISIBILITY_MASK; @@ -812,16 +830,16 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } } - + boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || - (action == MotionEvent.ACTION_CANCEL); + (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { // Note, we've already copied the previous state to our local // variable, so this takes effect on the next event mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } - + // The event wasn't an ACTION_DOWN, dispatch it to our target if // we have one. final View target = mMotionTarget; @@ -868,18 +886,18 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@inheritDoc} */ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { - + if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } - + if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } - + // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); @@ -1020,6 +1038,15 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + boolean populated = false; + for (int i = 0, count = getChildCount(); i < count; i++) { + populated |= getChildAt(i).dispatchPopulateAccessibilityEvent(event); + } + return populated; + } + /** * {@inheritDoc} */ @@ -1139,7 +1166,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { child.setDrawingCacheEnabled(true); - child.buildDrawingCache(); + child.buildDrawingCache(true); } } @@ -1181,7 +1208,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager bindLayoutAnimation(child); if (cache) { child.setDrawingCacheEnabled(true); - child.buildDrawingCache(); + child.buildDrawingCache(true); } } } @@ -1274,7 +1301,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager post(end); } } - + /** * Returns the index of the child to draw for this iteration. Override this * if you want to change the drawing order of children. By default, it @@ -1282,14 +1309,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * <p> * NOTE: In order for this method to be called, the * {@link #FLAG_USE_CHILD_DRAWING_ORDER} must be set. - * + * * @param i The current iteration. * @return The index of the child to draw this iteration. */ protected int getChildDrawingOrder(int childCount, int i) { return i; } - + private void notifyAnimationListener() { mGroupFlags &= ~FLAG_NOTIFY_ANIMATION_LISTENER; mGroupFlags |= FLAG_ANIMATION_DONE; @@ -1403,9 +1430,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } - // Clear the flag as early as possible to allow draw() implementations + // Sets the flag as early as possible to allow draw() implementations // to call invalidate() successfully when doing animations - child.mPrivateFlags = (child.mPrivateFlags & ~DIRTY_MASK) | DRAWN; + child.mPrivateFlags |= DRAWN; if (!concatMatrix && canvas.quickReject(cl, ct, cr, cb, Canvas.EdgeType.BW) && (child.mPrivateFlags & DRAW_ANIMATION) == 0) { @@ -1417,10 +1444,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final int sx = child.mScrollX; final int sy = child.mScrollY; + boolean scalingRequired = false; Bitmap cache = null; if ((flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE || (flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) { - cache = child.getDrawingCache(); + cache = child.getDrawingCache(true); + if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired; } final boolean hasNoCache = cache == null; @@ -1430,6 +1459,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager canvas.translate(cl - sx, ct - sy); } else { canvas.translate(cl, ct); + if (scalingRequired) { + // mAttachInfo cannot be null, otherwise scalingRequired == false + final float scale = 1.0f / mAttachInfo.mApplicationScale; + canvas.scale(scale, scale); + } } float alpha = 1.0f; @@ -1472,7 +1506,11 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (hasNoCache) { canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct)); } else { - canvas.clipRect(0, 0, cr - cl, cb - ct); + if (!scalingRequired) { + canvas.clipRect(0, 0, cr - cl, cb - ct); + } else { + canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight()); + } } } @@ -1482,6 +1520,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW); } + child.mPrivateFlags &= ~DIRTY_MASK; child.dispatchDraw(canvas); } else { child.draw(canvas); @@ -1546,7 +1585,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager children[i].setSelected(selected); } } - + @Override protected void dispatchSetPressed(boolean pressed) { final View[] children = mChildren; @@ -1577,7 +1616,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * {@inheritDoc} * - * @see #setStaticTransformationsEnabled(boolean) + * @see #setStaticTransformationsEnabled(boolean) */ protected boolean getChildStaticTransformation(View child, Transformation t) { return false; @@ -1844,10 +1883,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (child.hasFocus()) { requestChildFocus(child, child.findFocus()); } - + AttachInfo ai = mAttachInfo; if (ai != null) { - boolean lastKeepOn = ai.mKeepScreenOn; + boolean lastKeepOn = ai.mKeepScreenOn; ai.mKeepScreenOn = false; child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK)); if (ai.mKeepScreenOn) { @@ -2047,7 +2086,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } needGlobalAttributesUpdate(false); - + removeFromArray(index); if (clearChildFocus) { @@ -2080,7 +2119,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } needGlobalAttributesUpdate(false); - + if (notifyListener) { onHierarchyChangeListener.onChildViewRemoved(this, view); } @@ -2128,7 +2167,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager View clearChildFocus = null; needGlobalAttributesUpdate(false); - + for (int i = count - 1; i >= 0; i--) { final View view = children[i]; @@ -2173,7 +2212,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (child == mFocused) { child.clearFocus(); } - + if (animate && child.getAnimation() != null) { addDisappearingView(child); } else if (child.mAttachInfo != null) { @@ -2323,7 +2362,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final boolean drawAnimation = (child.mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION; // Check whether the child that requests the invalidate is fully opaque - final boolean isOpaque = child.isOpaque(); + final boolean isOpaque = child.isOpaque() && !drawAnimation && + child.getAnimation() != null; // Mark the child as dirty, using the appropriate flag // Make sure we do not set both flags at the same time final int opaqueFlag = isOpaque ? DIRTY_OPAQUE : DIRTY; @@ -3135,7 +3175,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } } } - + @Override protected boolean fitSystemWindows(Rect insets) { @@ -3269,7 +3309,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * laid out. See * {@link android.R.styleable#ViewGroup_Layout ViewGroup Layout Attributes} * for a list of all child view attributes that this class supports. - * + * * <p> * The base LayoutParams class just describes how big the view wants to be * for both width and height. For each dimension, it can specify one of: @@ -3400,7 +3440,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @param output the String to prepend to the internal representation * @return a String with the following format: output + * "ViewGroup.LayoutParams={ width=WIDTH, height=HEIGHT }" - * + * * @hide */ public String debug(String output) { @@ -3413,7 +3453,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * * @param size the size to convert * @return a String instance representing the supplied size - * + * * @hide */ protected static String sizeToString(int size) { diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index 5090c56..6f6e224 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -30,14 +30,18 @@ import android.os.Process; import android.os.SystemProperties; import android.util.AndroidRuntimeException; import android.util.Config; +import android.util.DisplayMetrics; import android.util.Log; import android.util.EventLog; import android.util.SparseArray; import android.view.View.MeasureSpec; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; import android.content.pm.PackageManager; +import android.content.res.CompatibilityInfo; import android.content.Context; import android.app.ActivityManagerNative; import android.Manifest; @@ -90,18 +94,18 @@ public final class ViewRoot extends Handler implements ViewParent, static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>(); - private static int sDrawTime; + private static int sDrawTime; long mLastTrackballTime = 0; final TrackballAxis mTrackballAxisX = new TrackballAxis(); final TrackballAxis mTrackballAxisY = new TrackballAxis(); final int[] mTmpLocation = new int[2]; - + final InputMethodCallback mInputMethodCallback; final SparseArray<Object> mPendingEvents = new SparseArray<Object>(); int mPendingEventSeq = 0; - + final Thread mThread; final WindowLeaked mLocation; @@ -123,16 +127,13 @@ public final class ViewRoot extends Handler implements ViewParent, int mHeight; Rect mDirty; // will be a graphics.Region soon boolean mIsAnimating; - // TODO: change these to scalar class. - private float mAppScale; - private float mAppScaleInverted; // = 1.0f / mAppScale - private int[] mWindowLayoutParamsBackup = null; + + CompatibilityInfo.Translator mTranslator; final View.AttachInfo mAttachInfo; final Rect mTempRect; // used in the transaction to not thrash the heap. final Rect mVisRect; // used to retrieve visible rect of focused view. - final Point mVisPoint; // used to retrieve global offset of focused view. boolean mTraversalScheduled; boolean mWillDrawSoon; @@ -168,7 +169,7 @@ public final class ViewRoot extends Handler implements ViewParent, int mScrollY; int mCurScrollY; Scroller mScroller; - + EGL10 mEgl; EGLDisplay mEglDisplay; EGLContext mEglContext; @@ -178,7 +179,7 @@ public final class ViewRoot extends Handler implements ViewParent, boolean mUseGL; boolean mGlWanted; - final ViewConfiguration mViewConfiguration; + final ViewConfiguration mViewConfiguration; /** * see {@link #playSoundEffect(int)} @@ -216,7 +217,6 @@ public final class ViewRoot extends Handler implements ViewParent, mDirty = new Rect(); mTempRect = new Rect(); mVisRect = new Rect(); - mVisPoint = new Point(); mWinFrame = new Rect(); mWindow = new W(this, context); mInputMethodCallback = new InputMethodCallback(this); @@ -384,29 +384,39 @@ public final class ViewRoot extends Handler implements ViewParent, synchronized (this) { if (mView == null) { mView = view; - mAppScale = mView.getContext().getApplicationScale(); - if (mAppScale != 1.0f) { - mWindowLayoutParamsBackup = new int[4]; - } - mAppScaleInverted = 1.0f / mAppScale; mWindowAttributes.copyFrom(attrs); + + CompatibilityInfo compatibilityInfo = + mView.getContext().getResources().getCompatibilityInfo(); + mTranslator = compatibilityInfo.getTranslator(attrs); + boolean restore = false; + if (attrs != null && mTranslator != null) { + restore = true; + attrs.backup(); + mTranslator.translateWindowLayout(attrs); + } + if (DEBUG_LAYOUT) Log.d(TAG, "WindowLayout in setView:" + attrs); + mSoftInputMode = attrs.softInputMode; mWindowAttributesChanged = true; mAttachInfo.mRootView = view; + mAttachInfo.mScalingRequired = + mTranslator == null ? false : mTranslator.scalingRequired; + mAttachInfo.mApplicationScale = + mTranslator == null ? 1.0f : mTranslator.applicationScale; if (panelParentView != null) { mAttachInfo.mPanelParentWindowToken = panelParentView.getApplicationWindowToken(); } mAdded = true; int res; /* = WindowManagerImpl.ADD_OKAY; */ - + // Schedule the first layout -before- adding to the window // manager, to make sure we do the relayout before receiving // any other events from the system. requestLayout(); - try { - res = sWindowSession.add(mWindow, attrs, + res = sWindowSession.add(mWindow, mWindowAttributes, getHostVisibility(), mAttachInfo.mContentInsets); } catch (RemoteException e) { mAdded = false; @@ -414,8 +424,15 @@ public final class ViewRoot extends Handler implements ViewParent, mAttachInfo.mRootView = null; unscheduleTraversals(); throw new RuntimeException("Adding window failed", e); + } finally { + if (restore) { + attrs.restore(); + } + } + + if (mTranslator != null) { + mTranslator.translateRectInScreenToAppWindow(mAttachInfo.mContentInsets); } - mAttachInfo.mContentInsets.scale(mAppScaleInverted); mPendingContentInsets.set(mAttachInfo.mContentInsets); mPendingVisibleInsets.set(0, 0, 0, 0); if (Config.LOGV) Log.v("ViewRoot", "Added window " + mWindow); @@ -526,18 +543,20 @@ public final class ViewRoot extends Handler implements ViewParent, public void invalidateChild(View child, Rect dirty) { checkThread(); - if (LOCAL_LOGV) Log.v(TAG, "Invalidate child: " + dirty); - if (mCurScrollY != 0 || mAppScale != 1.0f) { + if (DEBUG_DRAW) Log.v(TAG, "Invalidate child: " + dirty); + if (mCurScrollY != 0 || mTranslator != null) { mTempRect.set(dirty); + dirty = mTempRect; if (mCurScrollY != 0) { - mTempRect.offset(0, -mCurScrollY); + dirty.offset(0, -mCurScrollY); } - if (mAppScale != 1.0f) { - mTempRect.scale(mAppScale); + if (mTranslator != null) { + mTranslator.translateRectInAppWindowToScreen(dirty); + } + if (mAttachInfo.mScalingRequired) { + dirty.inset(-1, -1); } - dirty = mTempRect; } - // TODO: When doing a union with mDirty != empty, we must cancel all the DIRTY_OPAQUE flags mDirty.union(dirty); if (!mWillDrawSoon) { scheduleTraversals(); @@ -553,7 +572,7 @@ public final class ViewRoot extends Handler implements ViewParent, return null; } - public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) { + public boolean getChildVisibleRect(View child, Rect r, android.graphics.Point offset) { if (child != mView) { throw new RuntimeException("child is not mine, honest!"); } @@ -582,7 +601,7 @@ public final class ViewRoot extends Handler implements ViewParent, int getHostVisibility() { return mAppVisible ? mView.getVisibility() : View.GONE; } - + private void performTraversals() { // cache mView since it is used so much below... final View host = mView; @@ -614,19 +633,22 @@ public final class ViewRoot extends Handler implements ViewParent, boolean viewVisibilityChanged = mViewVisibility != viewVisibility || mNewSurfaceNeeded; + float appScale = mAttachInfo.mApplicationScale; + WindowManager.LayoutParams params = null; if (mWindowAttributesChanged) { mWindowAttributesChanged = false; params = lp; } - + Rect frame = mWinFrame; if (mFirst) { fullRedrawNeeded = true; mLayoutRequested = true; - Display d = new Display(0); - desiredWindowWidth = (int) (d.getWidth() * mAppScaleInverted); - desiredWindowHeight = (int) (d.getHeight() * mAppScaleInverted); + DisplayMetrics packageMetrics = + mView.getContext().getResources().getDisplayMetrics(); + desiredWindowWidth = packageMetrics.widthPixels; + desiredWindowHeight = packageMetrics.heightPixels; // For the very first time, tell the view hierarchy that it // is attached to the window. Note that at this point the surface @@ -641,12 +663,13 @@ public final class ViewRoot extends Handler implements ViewParent, host.dispatchAttachedToWindow(attachInfo, 0); getRunQueue().executeActions(attachInfo.mHandler); //Log.i(TAG, "Screen on initialized: " + attachInfo.mKeepScreenOn); + } else { - desiredWindowWidth = mWinFrame.width(); - desiredWindowHeight = mWinFrame.height(); + desiredWindowWidth = frame.width(); + desiredWindowHeight = frame.height(); if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) { if (DEBUG_ORIENTATION) Log.v("ViewRoot", - "View " + host + " resized to: " + mWinFrame); + "View " + host + " resized to: " + frame); fullRedrawNeeded = true; mLayoutRequested = true; windowResizesToFitContent = true; @@ -669,7 +692,7 @@ public final class ViewRoot extends Handler implements ViewParent, } boolean insetsChanged = false; - + if (mLayoutRequested) { if (mFirst) { host.fitSystemWindows(mAttachInfo.mContentInsets); @@ -694,9 +717,10 @@ public final class ViewRoot extends Handler implements ViewParent, || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { windowResizesToFitContent = true; - Display d = new Display(0); - desiredWindowWidth = (int) (d.getWidth() * mAppScaleInverted); - desiredWindowHeight = (int) (d.getHeight() * mAppScaleInverted); + DisplayMetrics packageMetrics = + mView.getContext().getResources().getDisplayMetrics(); + desiredWindowWidth = packageMetrics.widthPixels; + desiredWindowHeight = packageMetrics.heightPixels; } } @@ -753,7 +777,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } } - + if (params != null && (host.mPrivateFlags & View.REQUEST_TRANSPARENT_REGIONS) != 0) { if (!PixelFormat.formatHasAlpha(params.format)) { params.format = PixelFormat.TRANSLUCENT; @@ -782,7 +806,7 @@ public final class ViewRoot extends Handler implements ViewParent, // computed insets. insetsPending = computesInternalInsets && (mFirst || viewVisibilityChanged); - + if (mWindowAttributes.memoryType == WindowManager.LayoutParams.MEMORY_TYPE_GPU) { if (params == null) { params = mWindowAttributes; @@ -791,7 +815,6 @@ public final class ViewRoot extends Handler implements ViewParent, } } - final Rect frame = mWinFrame; boolean initialized = false; boolean contentInsetsChanged = false; boolean visibleInsetsChanged; @@ -818,7 +841,7 @@ public final class ViewRoot extends Handler implements ViewParent, + " content=" + mPendingContentInsets.toShortString() + " visible=" + mPendingVisibleInsets.toShortString() + " surface=" + mSurface); - + contentInsetsChanged = !mPendingContentInsets.equals( mAttachInfo.mContentInsets); visibleInsetsChanged = !mPendingVisibleInsets.equals( @@ -846,7 +869,7 @@ public final class ViewRoot extends Handler implements ViewParent, // all at once. newSurface = true; fullRedrawNeeded = true; - + if (mGlWanted && !mUseGL) { initializeGL(); initialized = mGlCanvas != null; @@ -864,7 +887,7 @@ public final class ViewRoot extends Handler implements ViewParent, } catch (RemoteException e) { } if (DEBUG_ORIENTATION) Log.v( - "ViewRoot", "Relayout returned: frame=" + mWinFrame + ", surface=" + mSurface); + "ViewRoot", "Relayout returned: frame=" + frame + ", surface=" + mSurface); attachInfo.mWindowLeft = frame.left; attachInfo.mWindowTop = frame.top; @@ -876,7 +899,7 @@ public final class ViewRoot extends Handler implements ViewParent, mHeight = frame.height(); if (initialized) { - mGlCanvas.setViewport((int) (mWidth * mAppScale), (int) (mHeight * mAppScale)); + mGlCanvas.setViewport((int) (mWidth * appScale), (int) (mHeight * appScale)); } boolean focusChangedDueToTouchMode = ensureTouchModeLocally( @@ -891,7 +914,7 @@ public final class ViewRoot extends Handler implements ViewParent, + " mHeight=" + mHeight + " measuredHeight" + host.mMeasuredHeight + " coveredInsetsChanged=" + contentInsetsChanged); - + // Ask host how big it wants to be host.measure(childWidthMeasureSpec, childHeightMeasureSpec); @@ -939,7 +962,6 @@ public final class ViewRoot extends Handler implements ViewParent, if (Config.DEBUG && ViewDebug.profileLayout) { startTime = SystemClock.elapsedRealtime(); } - host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight); if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) { @@ -966,11 +988,10 @@ public final class ViewRoot extends Handler implements ViewParent, mTmpLocation[1] + host.mBottom - host.mTop); host.gatherTransparentRegion(mTransparentRegion); + if (mTranslator != null) { + mTranslator.translateRegionInWindowToScreen(mTransparentRegion); + } - // TODO: scale the region, like: - // Region uses native methods. We probabl should have ScalableRegion class. - - // Region does not have equals method ? if (!mTransparentRegion.equals(mPreviousTransparentRegion)) { mPreviousTransparentRegion.set(mTransparentRegion); // reconfigure window manager @@ -981,7 +1002,6 @@ public final class ViewRoot extends Handler implements ViewParent, } } - if (DBG) { System.out.println("======================================"); System.out.println("performTraversals -- after setFrame"); @@ -1001,20 +1021,23 @@ public final class ViewRoot extends Handler implements ViewParent, givenContent.left = givenContent.top = givenContent.right = givenContent.bottom = givenVisible.left = givenVisible.top = givenVisible.right = givenVisible.bottom = 0; - insets.contentInsets.scale(mAppScale); - insets.visibleInsets.scale(mAppScale); - attachInfo.mTreeObserver.dispatchOnComputeInternalInsets(insets); + Rect contentInsets = insets.contentInsets; + Rect visibleInsets = insets.visibleInsets; + if (mTranslator != null) { + contentInsets = mTranslator.getTranslatedContentInsets(contentInsets); + visibleInsets = mTranslator.getTranslatedVisbileInsets(visibleInsets); + } if (insetsPending || !mLastGivenInsets.equals(insets)) { mLastGivenInsets.set(insets); try { sWindowSession.setInsets(mWindow, insets.mTouchableInsets, - insets.contentInsets, insets.visibleInsets); + contentInsets, visibleInsets); } catch (RemoteException e) { } } } - + if (mFirst) { // handle first focus request if (DEBUG_INPUT_RESIZE) Log.v(TAG, "First: mView.hasFocus()=" @@ -1052,7 +1075,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } } - + boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw(); if (!cancelDraw && !newSurface) { @@ -1140,10 +1163,9 @@ public final class ViewRoot extends Handler implements ViewParent, mAttachInfo.mViewScrollChanged = false; mAttachInfo.mTreeObserver.dispatchOnScrollChanged(); } - + int yoff; - final boolean scrolling = mScroller != null - && mScroller.computeScrollOffset(); + final boolean scrolling = mScroller != null && mScroller.computeScrollOffset(); if (scrolling) { yoff = mScroller.getCurrY(); } else { @@ -1153,26 +1175,28 @@ public final class ViewRoot extends Handler implements ViewParent, mCurScrollY = yoff; fullRedrawNeeded = true; } + float appScale = mAttachInfo.mApplicationScale; + boolean scalingRequired = mAttachInfo.mScalingRequired; Rect dirty = mDirty; if (mUseGL) { if (!dirty.isEmpty()) { Canvas canvas = mGlCanvas; - if (mGL!=null && canvas != null) { + if (mGL != null && canvas != null) { mGL.glDisable(GL_SCISSOR_TEST); mGL.glClearColor(0, 0, 0, 0); mGL.glClear(GL_COLOR_BUFFER_BIT); mGL.glEnable(GL_SCISSOR_TEST); mAttachInfo.mDrawingTime = SystemClock.uptimeMillis(); + mAttachInfo.mIgnoreDirtyState = true; mView.mPrivateFlags |= View.DRAWN; - float scale = mAppScale; int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); try { canvas.translate(0, -yoff); - if (scale != 1.0f) { - canvas.scale(scale, scale); + if (mTranslator != null) { + mTranslator.translateCanvas(canvas); } mView.draw(canvas); if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) { @@ -1182,6 +1206,8 @@ public final class ViewRoot extends Handler implements ViewParent, canvas.restoreToCount(saveCount); } + mAttachInfo.mIgnoreDirtyState = false; + mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); checkEglErrors(); @@ -1201,20 +1227,33 @@ public final class ViewRoot extends Handler implements ViewParent, return; } - if (fullRedrawNeeded) - dirty.union(0, 0, (int) (mWidth * mAppScale), (int) (mHeight * mAppScale)); + if (fullRedrawNeeded) { + mAttachInfo.mIgnoreDirtyState = true; + dirty.union(0, 0, (int) (mWidth * appScale), (int) (mHeight * appScale)); + } if (DEBUG_ORIENTATION || DEBUG_DRAW) { Log.v("ViewRoot", "Draw " + mView + "/" + mWindowAttributes.getTitle() + ": dirty={" + dirty.left + "," + dirty.top + "," + dirty.right + "," + dirty.bottom + "} surface=" - + surface + " surface.isValid()=" + surface.isValid()); + + surface + " surface.isValid()=" + surface.isValid() + ", appScale:" + + appScale + ", width=" + mWidth + ", height=" + mHeight); } Canvas canvas; try { + int left = dirty.left; + int top = dirty.top; + int right = dirty.right; + int bottom = dirty.bottom; canvas = surface.lockCanvas(dirty); + + if (left != dirty.left || top != dirty.top || right != dirty.right || + bottom != dirty.bottom) { + mAttachInfo.mIgnoreDirtyState = true; + } + // TODO: Do this in native canvas.setDensityScale(mDensity); } catch (Surface.OutOfResourcesException e) { @@ -1242,12 +1281,11 @@ public final class ViewRoot extends Handler implements ViewParent, // need to clear it before drawing so that the child will // properly re-composite its drawing on a transparent // background. This automatically respects the clip/dirty region - if (!canvas.isOpaque()) { - canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR); - } else if (yoff != 0) { - // If we are applying an offset, we need to clear the area - // where the offset doesn't appear to avoid having garbage - // left in the blank areas. + // or + // If we are applying an offset, we need to clear the area + // where the offset doesn't appear to avoid having garbage + // left in the blank areas. + if (!canvas.isOpaque() || yoff != 0) { canvas.drawColor(0, PorterDuff.Mode.CLEAR); } @@ -1256,27 +1294,27 @@ public final class ViewRoot extends Handler implements ViewParent, mAttachInfo.mDrawingTime = SystemClock.uptimeMillis(); mView.mPrivateFlags |= View.DRAWN; - float scale = mAppScale; - Context cxt = mView.getContext(); if (DEBUG_DRAW) { - Log.i(TAG, "Drawing: package:" + cxt.getPackageName() + ", appScale=" + mAppScale); + Context cxt = mView.getContext(); + Log.i(TAG, "Drawing: package:" + cxt.getPackageName() + + ", metrics=" + mView.getContext().getResources().getDisplayMetrics()); } - int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); + int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); try { canvas.translate(0, -yoff); - if (scale != 1.0f) { - // re-scale this - canvas.scale(scale, scale); + if (mTranslator != null) { + mTranslator.translateCanvas(canvas); } mView.draw(canvas); - - if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) { - mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING); - } } finally { + mAttachInfo.mIgnoreDirtyState = false; canvas.restoreToCount(saveCount); } + if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) { + mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING); + } + if (Config.DEBUG && ViewDebug.showFps) { int now = (int)SystemClock.elapsedRealtime(); if (sDrawTime != 0) { @@ -1289,7 +1327,7 @@ public final class ViewRoot extends Handler implements ViewParent, EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime); } } - + } finally { surface.unlockCanvasAndPost(canvas); } @@ -1297,7 +1335,7 @@ public final class ViewRoot extends Handler implements ViewParent, if (LOCAL_LOGV) { Log.v("ViewRoot", "Surface " + surface + " unlockCanvasAndPost"); } - + if (scrolling) { mFullRedrawNeeded = true; scheduleTraversals(); @@ -1310,7 +1348,7 @@ public final class ViewRoot extends Handler implements ViewParent, final Rect vi = attachInfo.mVisibleInsets; int scrollY = 0; boolean handled = false; - + if (vi.left > ci.left || vi.top > ci.top || vi.right > ci.right || vi.bottom > ci.bottom) { // We'll assume that we aren't going to change the scroll @@ -1397,7 +1435,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); @@ -1411,10 +1449,10 @@ public final class ViewRoot extends Handler implements ViewParent, } mScrollY = scrollY; } - + return handled; } - + public void requestChildFocus(View child, View focused) { checkThread(); if (mFocusedView != focused) { @@ -1494,7 +1532,7 @@ public final class ViewRoot extends Handler implements ViewParent, } catch (RemoteException e) { } } - + /** * Return true if child is an ancestor of parent, (or equal to the parent). */ @@ -1568,10 +1606,9 @@ public final class ViewRoot extends Handler implements ViewParent, } else { didFinish = event.getAction() == MotionEvent.ACTION_OUTSIDE; } - if (event != null) { - event.scale(mAppScaleInverted); + if (event != null && mTranslator != null) { + mTranslator.translateEventInScreenToAppWindow(event); } - try { boolean handled; if (mView != null && mAdded && event != null) { @@ -1657,6 +1694,7 @@ public final class ViewRoot extends Handler implements ViewParent, case RESIZED: Rect coveredInsets = ((Rect[])msg.obj)[0]; Rect visibleInsets = ((Rect[])msg.obj)[1]; + if (mWinFrame.width() == msg.arg1 && mWinFrame.height() == msg.arg2 && mPendingContentInsets.equals(coveredInsets) && mPendingVisibleInsets.equals(visibleInsets)) { @@ -1691,16 +1729,17 @@ public final class ViewRoot extends Handler implements ViewParent, if (mGlWanted && !mUseGL) { initializeGL(); if (mGlCanvas != null) { - mGlCanvas.setViewport((int) (mWidth * mAppScale), - (int) (mHeight * mAppScale)); + float appScale = mAttachInfo.mApplicationScale; + mGlCanvas.setViewport( + (int) (mWidth * appScale), (int) (mHeight * appScale)); } } } } - + mLastWasImTarget = WindowManager.LayoutParams .mayUseInputMethod(mWindowAttributes.flags); - + InputMethodManager imm = InputMethodManager.peekInstance(); if (mView != null) { if (hasWindowFocus && imm != null && mLastWasImTarget) { @@ -1708,7 +1747,7 @@ public final class ViewRoot extends Handler implements ViewParent, } mView.dispatchWindowFocusChanged(hasWindowFocus); } - + // Note: must be done after the focus change callbacks, // so all of the view state is set up correctly. if (hasWindowFocus) { @@ -1726,6 +1765,10 @@ public final class ViewRoot extends Handler implements ViewParent, ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; mHasHadWindowFocus = true; } + + if (hasWindowFocus && mView != null) { + sendAccessibilityEvents(); + } } } break; case DIE: @@ -1892,9 +1935,6 @@ public final class ViewRoot extends Handler implements ViewParent, } else { didFinish = false; } - if (event != null) { - event.scale(mAppScaleInverted); - } if (DEBUG_TRACKBALL) Log.v(TAG, "Motion event:" + event); @@ -2120,50 +2160,50 @@ public final class ViewRoot extends Handler implements ViewParent, } /** - * log motion events + * log motion events */ private static void captureMotionLog(String subTag, MotionEvent ev) { - //check dynamic switch + //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(','); + } + + 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.d(TAG, sb.toString()); } /** - * log motion events + * log motion events */ private static void captureKeyLog(String subTag, KeyEvent ev) { - //check dynamic switch - if (ev == null || + //check dynamic switch + if (ev == null || SystemProperties.getInt(ViewDebug.SYSTEM_PROPERTY_CAPTURE_EVENT, 0) == 0) { return; } - StringBuilder sb = new StringBuilder(subTag + ": "); + 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.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()); - } + Log.d(TAG, sb.toString()); + } int enqueuePendingEvent(Object event, boolean sendDone) { int seq = mPendingEventSeq+1; @@ -2181,7 +2221,7 @@ public final class ViewRoot extends Handler implements ViewParent, } return event; } - + private void deliverKeyEvent(KeyEvent event, boolean sendDone) { // If mView is null, we just consume the key event because it doesn't // make sense to do anything else with it. @@ -2238,7 +2278,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } } - + private void deliverKeyEventToViewHierarchy(KeyEvent event, boolean sendDone) { try { if (mView != null && mAdded) { @@ -2247,8 +2287,8 @@ public final class ViewRoot extends Handler implements ViewParent, if (checkForLeavingTouchModeAndConsume(event)) { return; - } - + } + if (Config.LOGV) { captureKeyLog("captureDispatchKeyEvent", event); } @@ -2324,24 +2364,31 @@ public final class ViewRoot extends Handler implements ViewParent, private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility, boolean insetsPending) throws RemoteException { + float appScale = mAttachInfo.mApplicationScale; boolean restore = false; - if (params != null && mAppScale != 1.0f) { + if (params != null && mTranslator != null) { restore = true; - params.scale(mAppScale, mWindowLayoutParamsBackup); + params.backup(); + mTranslator.translateWindowLayout(params); + } + if (params != null) { + if (DBG) Log.d(TAG, "WindowLayout in layoutWindow:" + params); } int relayoutResult = sWindowSession.relayout( mWindow, params, - (int) (mView.mMeasuredWidth * mAppScale), - (int) (mView.mMeasuredHeight * mAppScale), + (int) (mView.mMeasuredWidth * appScale), + (int) (mView.mMeasuredHeight * appScale), viewVisibility, insetsPending, mWinFrame, mPendingContentInsets, mPendingVisibleInsets, mSurface); if (restore) { - params.restore(mWindowLayoutParamsBackup); + params.restore(); + } + + if (mTranslator != null) { + mTranslator.translateRectInScreenToAppWinFrame(mWinFrame); + mTranslator.translateRectInScreenToAppWindow(mPendingContentInsets); + mTranslator.translateRectInScreenToAppWindow(mPendingVisibleInsets); } - - mPendingContentInsets.scale(mAppScaleInverted); - mPendingVisibleInsets.scale(mAppScaleInverted); - mWinFrame.scale(mAppScaleInverted); return relayoutResult; } @@ -2448,11 +2495,14 @@ public final class ViewRoot extends Handler implements ViewParent, + " visibleInsets=" + visibleInsets.toShortString() + " reportDraw=" + reportDraw); Message msg = obtainMessage(reportDraw ? RESIZED_REPORT :RESIZED); - - coveredInsets.scale(mAppScaleInverted); - visibleInsets.scale(mAppScaleInverted); - msg.arg1 = (int) (w * mAppScaleInverted); - msg.arg2 = (int) (h * mAppScaleInverted); + if (mTranslator != null) { + mTranslator.translateRectInScreenToAppWindow(coveredInsets); + mTranslator.translateRectInScreenToAppWindow(visibleInsets); + w *= mTranslator.applicationInvertedScale; + h *= mTranslator.applicationInvertedScale; + } + msg.arg1 = w; + msg.arg2 = h; msg.obj = new Rect[] { new Rect(coveredInsets), new Rect(visibleInsets) }; sendMessage(msg); } @@ -2511,6 +2561,21 @@ public final class ViewRoot extends Handler implements ViewParent, sendMessage(msg); } + /** + * The window is getting focus so if there is anything focused/selected + * send an {@link AccessibilityEvent} to announce that. + */ + private void sendAccessibilityEvents() { + if (!AccessibilityManager.getInstance(mView.getContext()).isEnabled()) { + return; + } + mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + View focusedView = mView.findFocus(); + if (focusedView != null && focusedView != mView) { + focusedView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + } + } + public boolean showContextMenuForChild(View originalView) { return false; } @@ -2540,14 +2605,14 @@ public final class ViewRoot extends Handler implements ViewParent, boolean immediate) { return scrollToRectOrFocus(rectangle, immediate); } - + static class InputMethodCallback extends IInputMethodCallback.Stub { private WeakReference<ViewRoot> mViewRoot; public InputMethodCallback(ViewRoot viewRoot) { mViewRoot = new WeakReference<ViewRoot>(viewRoot); } - + public void finishedEvent(int seq, boolean handled) { final ViewRoot viewRoot = mViewRoot.get(); if (viewRoot != null) { @@ -2559,13 +2624,13 @@ public final class ViewRoot extends Handler implements ViewParent, // Stub -- not for use in the client. } } - + static class EventCompletion extends Handler { final IWindow mWindow; final KeyEvent mKeyEvent; final boolean mIsPointer; final MotionEvent mMotionEvent; - + EventCompletion(Looper looper, IWindow window, KeyEvent key, boolean isPointer, MotionEvent motion) { super(looper); @@ -2575,7 +2640,7 @@ public final class ViewRoot extends Handler implements ViewParent, mMotionEvent = motion; sendEmptyMessage(0); } - + @Override public void handleMessage(Message msg) { if (mKeyEvent != null) { @@ -2617,7 +2682,7 @@ public final class ViewRoot extends Handler implements ViewParent, } } } - + static class W extends IWindow.Stub { private final WeakReference<ViewRoot> mViewRoot; private final Looper mMainLooper; @@ -2739,14 +2804,14 @@ public final class ViewRoot extends Handler implements ViewParent, * The maximum amount of acceleration we will apply. */ static final float MAX_ACCELERATION = 20; - + /** * The maximum amount of time (in milliseconds) between events in order * for us to consider the user to be doing fast trackball movements, * and thus apply an acceleration. */ static final long FAST_MOVE_TIME = 150; - + /** * Scaling factor to the time (in milliseconds) between events to how * much to multiple/divide the current acceleration. When movement @@ -2754,7 +2819,7 @@ public final class ViewRoot extends Handler implements ViewParent, * FAST_MOVE_TIME it divides it. */ static final float ACCEL_MOVE_SCALING_FACTOR = (1.0f/40); - + float position; float absPosition; float acceleration = 1; @@ -2806,7 +2871,7 @@ public final class ViewRoot extends Handler implements ViewParent, } else { normTime = 0; } - + // The number of milliseconds between each movement that is // considered "normal" and will not result in any acceleration // or deceleration, scaled by the offset we have here. @@ -2964,7 +3029,7 @@ public final class ViewRoot extends Handler implements ViewParent, sRunQueues.set(rq); return rq; } - + /** * @hide */ diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 428de67..d7457a0 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -24,7 +24,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; -import android.util.Log; +import android.view.accessibility.AccessibilityEvent; /** * Abstract base class for a top-level window look and behavior policy. An @@ -153,7 +153,16 @@ public abstract class Window { * @return boolean Return true if this event was consumed. */ public boolean dispatchTrackballEvent(MotionEvent event); - + + /** + * Called to process population of {@link AccessibilityEvent}s. + * + * @param event The event. + * + * @return boolean Return true if event population was completed. + */ + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); + /** * Instantiate the view to display in the panel for 'featureId'. * You can return null, in which case the default content (typically @@ -367,8 +376,14 @@ public abstract class Window { String title; if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA) { title="Media"; + } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY) { + title="MediaOvr"; } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) { title="Panel"; + } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL) { + title="SubPanel"; + } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG) { + title="AtchDlg"; } else { title=Integer.toString(wp.type); } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index c69c281..bdb86d7 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -18,7 +18,6 @@ package android.view; import android.content.pm.ActivityInfo; import android.graphics.PixelFormat; -import android.graphics.Rect; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; @@ -210,6 +209,15 @@ public interface WindowManager extends ViewManager { public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3; /** + * Window type: window for showing overlays on top of media windows. + * These windows are displayed between TYPE_APPLICATION_MEDIA and the + * application window. They should be translucent to be useful. This + * is a big ugly hack so: + * @hide + */ + public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW+4; + + /** * End of types of sub-windows. */ public static final int LAST_SUB_WINDOW = 1999; @@ -466,6 +474,21 @@ public interface WindowManager extends ViewManager { */ public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000; + /** Window flag: special flag to let windows be shown when the screen + * is locked. This will let application windows take precedence over + * key guard or any other lock screens. Can be used with + * {@link #FLAG_KEEP_SCREEN_ON} to turn screen on and display windows + * directly before showing the key guard window + * + * {@hide} */ + public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000; + + /** Window flag: special flag to let a window ignore the compatibility scaling. + * This is used by SurfaceView to create a window that does not scale the content. + * + * {@hide} */ + public static final int FLAG_NO_COMPATIBILITY_SCALING = 0x00100000; + /** Window flag: a special option intended for system dialogs. When * this flag is set, the window will demand focus unconditionally when * it is created. @@ -787,6 +810,7 @@ public interface WindowManager extends ViewManager { screenOrientation = in.readInt(); } + @SuppressWarnings({"PointlessBitwiseExpression"}) public static final int LAYOUT_CHANGED = 1<<0; public static final int TYPE_CHANGED = 1<<1; public static final int FLAGS_CHANGED = 1<<2; @@ -800,6 +824,9 @@ public interface WindowManager extends ViewManager { public static final int SCREEN_ORIENTATION_CHANGED = 1<<10; public static final int SCREEN_BRIGHTNESS_CHANGED = 1<<11; + // internal buffer to backup/restore parameters under compatibility mode. + private int[] mCompatibilityParamsBackup = null; + public final int copyFrom(LayoutParams o) { int changes = 0; @@ -957,36 +984,45 @@ public interface WindowManager extends ViewManager { /** * Scale the layout params' coordinates and size. - * Returns the original info as a backup so that the caller can - * restore the layout params; - */ - void scale(float scale, int[] backup) { - if (scale != 1.0f) { - backup[0] = x; - backup[1] = y; - x *= scale; - y *= scale; - if (width > 0) { - backup[2] = width; - width *= scale; - } - if (height > 0) { - backup[3] = height; - height *= scale; - } + * @hide + */ + public void scale(float scale) { + x *= scale; + y *= scale; + if (width > 0) { + width *= scale; + } + if (height > 0) { + height *= scale; } } /** - * Restore the layout params' coordinates and size. - */ - void restore(int[] backup) { - x = backup[0]; - y = backup[1]; - if (width > 0) { - width = backup[2]; + * Backup the layout parameters used in compatibility mode. + * @see LayoutParams#restore() + */ + void backup() { + int[] backup = mCompatibilityParamsBackup; + if (backup == null) { + // we backup 4 elements, x, y, width, height + backup = mCompatibilityParamsBackup = new int[4]; } - if (height > 0) { + backup[0] = x; + backup[1] = y; + backup[2] = width; + backup[3] = height; + } + + /** + * Restore the layout params' coordinates, size and gravity + * @see LayoutParams#backup() + */ + void restore() { + int[] backup = mCompatibilityParamsBackup; + if (backup != null) { + x = backup[0]; + y = backup[1]; + width = backup[2]; height = backup[3]; } } diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index 755d7b8..0973599 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -173,7 +173,6 @@ public class WindowManagerImpl implements WindowManager { mRoots[index] = root; mParams[index] = wparams; } - // do this last because it fires off messages to start doing things root.setView(view, wparams, panelParentView); } diff --git a/core/java/android/view/accessibility/AccessibilityEvent.aidl b/core/java/android/view/accessibility/AccessibilityEvent.aidl new file mode 100644 index 0000000..cee3604 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityEvent.aidl @@ -0,0 +1,19 @@ +/** + * 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.view.accessibility; + +parcelable AccessibilityEvent; diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java new file mode 100644 index 0000000..c22f991 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -0,0 +1,734 @@ +/* + * 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.view.accessibility; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class represents accessibility events that are sent by the system when + * something notable happens in the user interface. For example, when a + * {@link android.widget.Button} is clicked, a {@link android.view.View} is focused, etc. + * <p> + * 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 + * by this class. For each event type there is a corresponding constant defined + * in this class. Since some event types are semantically close there are mask + * constants that group them together. Follows a specification of the event + * types and their associated properties: + * <p> + * <b>VIEW TYPES</b> <br> + * <p> + * <b>View clicked</b> - represents the event of clicking on a {@link android.view.View} + * like {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc. <br> + * Type:{@link #TYPE_VIEW_CLICKED} <br> + * Properties: + * {@link #getClassName()}, + * {@link #getPackageName()}, + * {@link #getEventTime()}, + * {@link #getText()}, + * {@link #isChecked()}, + * {@link #isEnabled()}, + * {@link #isPassword()}, + * {@link #getItemCount()}, + * {@link #getCurrentItemIndex()} + * <p> + * <b>View long clicked</b> - represents the event of long clicking on a {@link android.view.View} + * like {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc. <br> + * Type:{@link #TYPE_VIEW_LONG_CLICKED} <br> + * Properties: + * {@link #getClassName()}, + * {@link #getPackageName()}, + * {@link #getEventTime()}, + * {@link #getText()}, + * {@link #isChecked()}, + * {@link #isEnabled()}, + * {@link #isPassword()}, + * {@link #getItemCount()}, + * {@link #getCurrentItemIndex()} + * <p> + * <b>View selected</b> - represents the event of selecting an item usually in + * the context of an {@link android.widget.AdapterView}. <br> + * Type: {@link #TYPE_VIEW_SELECTED} <br> + * Properties: + * {@link #getClassName()}, + * {@link #getPackageName()}, + * {@link #getEventTime()}, + * {@link #getText()}, + * {@link #isChecked()}, + * {@link #isEnabled()}, + * {@link #isPassword()}, + * {@link #getItemCount()}, + * {@link #getCurrentItemIndex()} + * <p> + * <b>View focused</b> - represents the event of focusing a + * {@link android.view.View}. <br> + * Type: {@link #TYPE_VIEW_FOCUSED} <br> + * Properties: + * {@link #getClassName()}, + * {@link #getPackageName()}, + * {@link #getEventTime()}, + * {@link #getText()}, + * {@link #isChecked()}, + * {@link #isEnabled()}, + * {@link #isPassword()}, + * {@link #getItemCount()}, + * {@link #getCurrentItemIndex()} + * <p> + * <b>View text changed</b> - represents the event of changing the text of an + * {@link android.widget.EditText}. <br> + * Type: {@link #TYPE_VIEW_TEXT_CHANGED} <br> + * Properties: + * {@link #getClassName()}, + * {@link #getPackageName()}, + * {@link #getEventTime()}, + * {@link #getText()}, + * {@link #isChecked()}, + * {@link #isEnabled()}, + * {@link #isPassword()}, + * {@link #getItemCount()}, + * {@link #getCurrentItemIndex()}, + * {@link #getFromIndex()}, + * {@link #getAddedCount()}, + * {@link #getRemovedCount()}, + * {@link #getBeforeText()} + * <p> + * <b>TRANSITION TYPES</b> <br> + * <p> + * <b>Window state changed</b> - represents the event of opening/closing a + * {@link android.widget.PopupWindow}, {@link android.view.Menu}, + * {@link android.app.Dialog}, etc. <br> + * Type: {@link #TYPE_WINDOW_STATE_CHANGED} <br> + * Properties: + * {@link #getClassName()}, + * {@link #getPackageName()}, + * {@link #getEventTime()}, + * {@link #getText()} + * <p> + * <b>NOTIFICATION TYPES</b> <br> + * <p> + * <b>Notification state changed</b> - represents the event showing/hiding + * {@link android.app.Notification}. + * Type: {@link #TYPE_NOTIFICATION_STATE_CHANGED} <br> + * Properties: + * {@link #getClassName()}, + * {@link #getPackageName()}, + * {@link #getEventTime()}, + * {@link #getText()} + * {@link #getParcelableData()} + * <p> + * <b>Security note</b> + * <p> + * Since an event contains the text of its source privacy can be compromised by leaking of + * sensitive information such as passwords. To address this issue any event fired in response + * to manipulation of a PASSWORD field does NOT CONTAIN the text of the password. + * + * @see android.view.accessibility.AccessibilityManager + * @see android.accessibilityservice.AccessibilityService + */ +public final class AccessibilityEvent implements Parcelable { + + /** + * Invalid selection/focus position. + * + * @see #getCurrentItemIndex() + */ + public static final int INVALID_POSITION = -1; + + /** + * Maximum length of the text fields. + * + * @see #getBeforeText() + * @see #getText() + */ + public static final int MAX_TEXT_LENGTH = 500; + + /** + * Represents the event of clicking on a {@link android.view.View} like + * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc. + */ + public static final int TYPE_VIEW_CLICKED = 0x00000001; + + /** + * Represents the event of long clicking on a {@link android.view.View} like + * {@link android.widget.Button}, {@link android.widget.CompoundButton}, etc. + */ + public static final int TYPE_VIEW_LONG_CLICKED = 0x00000002; + + /** + * Represents the event of selecting an item usually in the context of an + * {@link android.widget.AdapterView}. + */ + public static final int TYPE_VIEW_SELECTED = 0x00000004; + + /** + * Represents the event of focusing a {@link android.view.View}. + */ + public static final int TYPE_VIEW_FOCUSED = 0x00000008; + + /** + * Represents the event of changing the text of an {@link android.widget.EditText}. + */ + public static final int TYPE_VIEW_TEXT_CHANGED = 0x00000010; + + /** + * Represents the event of opening/closing a {@link android.widget.PopupWindow}, + * {@link android.view.Menu}, {@link android.app.Dialog}, etc. + */ + public static final int TYPE_WINDOW_STATE_CHANGED = 0x00000020; + + /** + * Represents the event showing/hiding a {@link android.app.Notification}. + */ + public static final int TYPE_NOTIFICATION_STATE_CHANGED = 0x00000040; + + /** + * Mask for {@link AccessibilityEvent} all types. + * + * @see #TYPE_VIEW_CLICKED + * @see #TYPE_VIEW_LONG_CLICKED + * @see #TYPE_VIEW_SELECTED + * @see #TYPE_VIEW_FOCUSED + * @see #TYPE_VIEW_TEXT_CHANGED + * @see #TYPE_WINDOW_STATE_CHANGED + * @see #TYPE_NOTIFICATION_STATE_CHANGED + */ + 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 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 int mEventType; + private int mBooleanProperties; + private int mCurrentItemIndex; + private int mItemCount; + private int mFromIndex; + private int mAddedCount; + private int mRemovedCount; + + private long mEventTime; + + private CharSequence mClassName; + private CharSequence mPackageName; + private CharSequence mContentDescription; + private CharSequence mBeforeText; + + private Parcelable mParcelableData; + + private final List<CharSequence> mText = new ArrayList<CharSequence>(); + + private boolean mIsInPool; + + /* + * Hide constructor from clients. + */ + private AccessibilityEvent() { + mCurrentItemIndex = INVALID_POSITION; + } + + /** + * Gets if the source is checked. + * + * @return True if the view is checked, false otherwise. + */ + public boolean isChecked() { + return getBooleanProperty(CHECKED); + } + + /** + * Sets if the source is checked. + * + * @param isChecked True if the view is checked, false otherwise. + */ + public void setChecked(boolean isChecked) { + setBooleanProperty(CHECKED, isChecked); + } + + /** + * Gets if the source is enabled. + * + * @return True if the view is enabled, false otherwise. + */ + public boolean isEnabled() { + return getBooleanProperty(ENABLED); + } + + /** + * Sets if the source is enabled. + * + * @param isEnabled True if the view is enabled, false otherwise. + */ + public void setEnabled(boolean isEnabled) { + setBooleanProperty(ENABLED, isEnabled); + } + + /** + * Gets if the source is a password field. + * + * @return True if the view is a password field, false otherwise. + */ + public boolean isPassword() { + return getBooleanProperty(PASSWORD); + } + + /** + * Sets if the source is a password field. + * + * @param isPassword True if the view is a password field, false otherwise. + */ + 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); + } + + /** + * Gets the event type. + * + * @return The event type. + */ + public int getEventType() { + return mEventType; + } + + /** + * Sets the event type. + * + * @param eventType The event type. + */ + public void setEventType(int eventType) { + mEventType = eventType; + } + + /** + * Gets the number of items that can be visited. + * + * @return The number of items. + */ + public int getItemCount() { + return mItemCount; + } + + /** + * Sets the number of items that can be visited. + * + * @param itemCount The number of items. + */ + public void setItemCount(int itemCount) { + mItemCount = itemCount; + } + + /** + * Gets the index of the source in the list of items the can be visited. + * + * @return The current item index. + */ + public int getCurrentItemIndex() { + return mCurrentItemIndex; + } + + /** + * Sets the index of the source in the list of items that can be visited. + * + * @param currentItemIndex The current item index. + */ + public void setCurrentItemIndex(int currentItemIndex) { + mCurrentItemIndex = currentItemIndex; + } + + /** + * Gets the index of the first character of the changed sequence. + * + * @return The index of the first character. + */ + public int getFromIndex() { + return mFromIndex; + } + + /** + * Sets the index of the first character of the changed sequence. + * + * @param fromIndex The index of the first character. + */ + public void setFromIndex(int fromIndex) { + mFromIndex = fromIndex; + } + + /** + * Gets the number of added characters. + * + * @return The number of added characters. + */ + public int getAddedCount() { + return mAddedCount; + } + + /** + * Sets the number of added characters. + * + * @param addedCount The number of added characters. + */ + public void setAddedCount(int addedCount) { + mAddedCount = addedCount; + } + + /** + * Gets the number of removed characters. + * + * @return The number of removed characters. + */ + public int getRemovedCount() { + return mRemovedCount; + } + + /** + * Sets the number of removed characters. + * + * @param removedCount The number of removed characters. + */ + public void setRemovedCount(int removedCount) { + mRemovedCount = removedCount; + } + + /** + * Gets the time in which this event was sent. + * + * @return The event time. + */ + public long getEventTime() { + return mEventTime; + } + + /** + * Sets the time in which this event was sent. + * + * @param eventTime The event time. + */ + public void setEventTime(long eventTime) { + 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. + */ + public CharSequence getPackageName() { + return mPackageName; + } + + /** + * Sets the package name of the source. + * + * @param packageName The package name. + */ + public void setPackageName(CharSequence packageName) { + 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. + * + * @param eventType The event type. + * @return An instance. + */ + public static AccessibilityEvent obtain(int eventType) { + AccessibilityEvent event = AccessibilityEvent.obtain(); + event.setEventType(eventType); + return event; + } + + /** + * Returns a cached instance if such is available or a new one is + * instantiated. + * + * @return An instance. + */ + public static AccessibilityEvent obtain() { + synchronized (mPoolLock) { + if (sPool != null) { + AccessibilityEvent event = sPool; + sPool = sPool.mNext; + sPoolSize--; + event.mNext = null; + event.mIsInPool = false; + return event; + } + return new AccessibilityEvent(); + } + } + + /** + * Return an instance back to be reused. + * <p> + * <b>Note: You must not touch the object after calling this function.</b> + */ + public void recycle() { + if (mIsInPool) { + return; + } + + clear(); + synchronized (mPoolLock) { + if (sPoolSize <= MAX_POOL_SIZE) { + mNext = sPool; + sPool = this; + mIsInPool = true; + sPoolSize++; + } + } + } + + /** + * Clears the state of this instance. + */ + private void clear() { + mEventType = 0; + mBooleanProperties = 0; + mCurrentItemIndex = INVALID_POSITION; + mItemCount = 0; + mFromIndex = 0; + mAddedCount = 0; + mRemovedCount = 0; + mEventTime = 0; + mClassName = null; + mPackageName = null; + mContentDescription = null; + mBeforeText = null; + mText.clear(); + } + + /** + * Gets the value of a boolean property. + * + * @param property The property. + * @return The value. + */ + private boolean getBooleanProperty(int property) { + return (mBooleanProperties & property) == property; + } + + /** + * Sets a boolean property. + * + * @param property The property. + * @param value The value. + */ + private void setBooleanProperty(int property, boolean value) { + if (value) { + mBooleanProperties |= property; + } else { + mBooleanProperties &= ~property; + } + } + + /** + * Creates a new instance from a {@link Parcel}. + * + * @param parcel A parcel containing the state of a {@link AccessibilityEvent}. + */ + 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) { + parcel.writeInt(mEventType); + parcel.writeInt(mBooleanProperties); + parcel.writeInt(mCurrentItemIndex); + parcel.writeInt(mItemCount); + parcel.writeInt(mFromIndex); + parcel.writeInt(mAddedCount); + parcel.writeInt(mRemovedCount); + parcel.writeLong(mEventTime); + TextUtils.writeToParcel(mClassName, parcel, 0); + TextUtils.writeToParcel(mPackageName, parcel, 0); + TextUtils.writeToParcel(mContentDescription, parcel, 0); + TextUtils.writeToParcel(mBeforeText, parcel, 0); + parcel.writeParcelable(mParcelableData, flags); + parcel.writeList(mText); + } + + public int describeContents() { + return 0; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(super.toString()); + builder.append("; EventType: " + mEventType); + builder.append("; EventTime: " + mEventTime); + builder.append("; ClassName: " + mClassName); + builder.append("; PackageName: " + mPackageName); + builder.append("; Text: " + mText); + builder.append("; ContentDescription: " + mContentDescription); + builder.append("; ItemCount: " + mItemCount); + builder.append("; CurrentItemIndex: " + mCurrentItemIndex); + builder.append("; IsEnabled: " + isEnabled()); + builder.append("; IsPassword: " + isPassword()); + builder.append("; IsChecked: " + isChecked()); + builder.append("; IsFullScreen: " + isFullScreen()); + builder.append("; BeforeText: " + mBeforeText); + builder.append("; FromIndex: " + mFromIndex); + builder.append("; AddedCount: " + mAddedCount); + builder.append("; RemovedCount: " + mRemovedCount); + builder.append("; ParcelableData: " + mParcelableData); + return builder.toString(); + } + + /** + * @see Parcelable.Creator + */ + public static final Parcelable.Creator<AccessibilityEvent> CREATOR = + new Parcelable.Creator<AccessibilityEvent>() { + public AccessibilityEvent createFromParcel(Parcel parcel) { + AccessibilityEvent event = AccessibilityEvent.obtain(); + event.initFromParcel(parcel); + return event; + } + + public AccessibilityEvent[] newArray(int size) { + return new AccessibilityEvent[size]; + } + }; +} diff --git a/core/java/android/view/accessibility/AccessibilityEventSource.java b/core/java/android/view/accessibility/AccessibilityEventSource.java new file mode 100644 index 0000000..3d70959 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityEventSource.java @@ -0,0 +1,52 @@ +/* + * 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.view.accessibility; + +/** + * This interface is implemented by classes source of {@link AccessibilityEvent}s. + */ +public interface AccessibilityEventSource { + + /** + * Handles the request for sending an {@link AccessibilityEvent} given + * the event type. The method must first check if accessibility is on + * via calling {@link AccessibilityManager#isEnabled()}, obtain + * an {@link AccessibilityEvent} from the event pool through calling + * {@link AccessibilityEvent#obtain(int)}, populate the event, and + * send it for dispatch via calling + * {@link AccessibilityManager#sendAccessibilityEvent(AccessibilityEvent)}. + * + * @see AccessibilityEvent + * @see AccessibilityManager + * + * @param eventType The event type. + */ + public void sendAccessibilityEvent(int eventType); + + /** + * Handles the request for sending an {@link AccessibilityEvent}. The + * method does not guarantee to check if accessibility is on before + * sending the event for dispatch. It is responsibility of the caller + * to do the check via calling {@link AccessibilityManager#isEnabled()}. + * + * @see AccessibilityEvent + * @see AccessibilityManager + * + * @param event The event. + */ + public void sendAccessibilityEventUnchecked(AccessibilityEvent event); +} diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java new file mode 100644 index 0000000..0186270 --- /dev/null +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -0,0 +1,198 @@ +/* + * 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.view.accessibility; + +import static android.util.Config.LOGV; + +import android.content.Context; +import android.content.pm.ServiceInfo; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemClock; +import android.util.Log; + +import java.util.Collections; +import java.util.List; + +/** + * System level service that serves as an event dispatch for {@link AccessibilityEvent}s. + * Such events are generated when something notable happens in the user interface, + * for example an {@link android.app.Activity} starts, the focus or selection of a + * {@link android.view.View} changes etc. Parties interested in handling accessibility + * events implement and register an accessibility service which extends + * {@link android.accessibilityservice.AccessibilityService}. + * + * @see AccessibilityEvent + * @see android.accessibilityservice.AccessibilityService + * @see android.content.Context#getSystemService + */ +public final class AccessibilityManager { + private static final String LOG_TAG = "AccessibilityManager"; + + static final Object sInstanceSync = new Object(); + + private static AccessibilityManager sInstance; + + private static final int DO_SET_ENABLED = 10; + + final IAccessibilityManager mService; + + final Handler mHandler; + + boolean mIsEnabled; + + final IAccessibilityManagerClient.Stub mClient = new IAccessibilityManagerClient.Stub() { + public void setEnabled(boolean enabled) { + mHandler.obtainMessage(DO_SET_ENABLED, enabled ? 1 : 0, 0).sendToTarget(); + } + }; + + class MyHandler extends Handler { + + MyHandler(Looper mainLooper) { + super(mainLooper); + } + + @Override + public void handleMessage(Message message) { + switch (message.what) { + case DO_SET_ENABLED : + synchronized (mHandler) { + mIsEnabled = (message.arg1 == 1); + } + return; + default : + Log.w(LOG_TAG, "Unknown message type: " + message.what); + } + } + } + + /** + * Get an AccessibilityManager instance (create one if necessary). + * + * @hide + */ + public static AccessibilityManager getInstance(Context context) { + synchronized (sInstanceSync) { + if (sInstance == null) { + sInstance = new AccessibilityManager(context); + } + } + return sInstance; + } + + /** + * Create an instance. + * + * @param context A {@link Context}. + */ + private AccessibilityManager(Context context) { + mHandler = new MyHandler(context.getMainLooper()); + IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE); + mService = IAccessibilityManager.Stub.asInterface(iBinder); + try { + mService.addClient(mClient); + } catch (RemoteException re) { + Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); + } + } + + /** + * Returns if the {@link AccessibilityManager} is enabled. + * + * @return True if this {@link AccessibilityManager} is enabled, false otherwise. + */ + public boolean isEnabled() { + synchronized (mHandler) { + return mIsEnabled; + } + } + + /** + * Sends an {@link AccessibilityEvent}. If this {@link AccessibilityManager} is not + * enabled the call is a NOOP. + * + * @param event The {@link AccessibilityEvent}. + * + * @throws IllegalStateException if a client tries to send an {@link AccessibilityEvent} + * while accessibility is not enabled. + */ + public void sendAccessibilityEvent(AccessibilityEvent event) { + if (!mIsEnabled) { + throw new IllegalStateException("Accessibility off. Did you forget to check that?"); + } + boolean doRecycle = false; + try { + event.setEventTime(SystemClock.uptimeMillis()); + // it is possible that this manager is in the same process as the service but + // client using it is called through Binder from another process. Example: MMS + // app adds a SMS notification and the NotificationManagerService calls this method + long identityToken = Binder.clearCallingIdentity(); + doRecycle = mService.sendAccessibilityEvent(event); + Binder.restoreCallingIdentity(identityToken); + if (LOGV) { + Log.i(LOG_TAG, event + " sent"); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error during sending " + event + " ", re); + } finally { + if (doRecycle) { + event.recycle(); + } + } + } + + /** + * Requests interruption of the accessibility feedback from all accessibility services. + */ + public void interrupt() { + if (!mIsEnabled) { + throw new IllegalStateException("Accessibility off. Did you forget to check that?"); + } + try { + mService.interrupt(); + if (LOGV) { + Log.i(LOG_TAG, "Requested interrupt from all services"); + } + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error while requesting interrupt from all services. ", re); + } + } + + /** + * Returns the {@link ServiceInfo}s of the installed accessibility services. + * + * @return An unmodifiable list with {@link ServiceInfo}s. + */ + public List<ServiceInfo> getAccessibilityServiceList() { + List<ServiceInfo> services = null; + try { + services = mService.getAccessibilityServiceList(); + if (LOGV) { + 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); + } +} diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl new file mode 100644 index 0000000..32788be --- /dev/null +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -0,0 +1,39 @@ +/* //device/java/android/android/app/INotificationManager.aidl +** +** Copyright 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.view.accessibility; + +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.IAccessibilityManagerClient; +import android.content.pm.ServiceInfo; + +/** + * Interface implemented by the AccessibilityManagerService called by + * the AccessibilityMasngers. + * + * @hide + */ +interface IAccessibilityManager { + + void addClient(IAccessibilityManagerClient client); + + boolean sendAccessibilityEvent(in AccessibilityEvent uiEvent); + + List<ServiceInfo> getAccessibilityServiceList(); + + void interrupt(); +} diff --git a/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl b/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl new file mode 100644 index 0000000..1eb60fc --- /dev/null +++ b/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl @@ -0,0 +1,29 @@ +/* + * 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.view.accessibility; + +/** + * Interface a client of the IAccessibilityManager implements to + * receive information about changes in the manager state. + * + * @hide + */ +oneway interface IAccessibilityManagerClient { + + void setEnabled(boolean enabled); + +} diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index 11de3e2..7393737 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -297,6 +297,10 @@ public class BaseInputConnection implements InputConnection { b = tmp; } + if (a <= 0) { + return ""; + } + if (length > a) { length = a; } @@ -336,10 +340,19 @@ public class BaseInputConnection implements InputConnection { } /** - * The default implementation does nothing. + * The default implementation turns this into the enter key. */ public boolean performEditorAction(int actionCode) { - return false; + long eventTime = SystemClock.uptimeMillis(); + sendKeyEvent(new KeyEvent(eventTime, eventTime, + KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE + | KeyEvent.FLAG_EDITOR_ACTION)); + sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, + KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE + | KeyEvent.FLAG_EDITOR_ACTION)); + return true; } /** @@ -488,12 +501,12 @@ public class BaseInputConnection implements InputConnection { } else { a = Selection.getSelectionStart(content); b = Selection.getSelectionEnd(content); - if (a >=0 && b>= 0 && a != b) { - if (b < a) { - int tmp = a; - a = b; - b = tmp; - } + if (a < 0) a = 0; + if (b < 0) b = 0; + if (b < a) { + int tmp = a; + a = b; + b = tmp; } } diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index ba3f78c..dbd2682 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -143,6 +143,17 @@ class BrowserFrame extends Handler { } /** + * Load a url with "POST" method from the network into the main frame. + * @param url The url to load. + * @param data The data for POST request. + */ + public void postUrl(String url, byte[] data) { + mLoadInitFromJava = true; + nativePostUrl(url, data); + mLoadInitFromJava = false; + } + + /** * Load the content as if it was loaded by the provided base URL. The * failUrl is used as the history entry for the load data. If null or * an empty string is passed for the failUrl, then no history entry is @@ -752,6 +763,8 @@ class BrowserFrame extends Handler { */ private native void nativeLoadUrl(String url); + private native void nativePostUrl(String url, byte[] postData); + private native void nativeLoadData(String baseUrl, String data, String mimeType, String encoding, String failUrl); diff --git a/core/java/android/webkit/ByteArrayBuilder.java b/core/java/android/webkit/ByteArrayBuilder.java index 806b458..145411c 100644 --- a/core/java/android/webkit/ByteArrayBuilder.java +++ b/core/java/android/webkit/ByteArrayBuilder.java @@ -17,6 +17,7 @@ package android.webkit; import java.util.LinkedList; +import java.util.ListIterator; /** Utility class optimized for accumulating bytes, and then spitting them back out. It does not optimize for returning the result in a @@ -94,6 +95,20 @@ class ByteArrayBuilder { return mChunks.isEmpty(); } + public int size() { + return mChunks.size(); + } + + public int getByteSize() { + int total = 0; + ListIterator<Chunk> it = mChunks.listIterator(0); + while (it.hasNext()) { + Chunk c = it.next(); + total += c.mLength; + } + return total; + } + public synchronized void clear() { Chunk c = getFirstChunk(); while (c != null) { diff --git a/core/java/android/webkit/FrameLoader.java b/core/java/android/webkit/FrameLoader.java index 6f1b160..66ab021 100644 --- a/core/java/android/webkit/FrameLoader.java +++ b/core/java/android/webkit/FrameLoader.java @@ -364,7 +364,7 @@ class FrameLoader { String cookie = CookieManager.getInstance().getCookie( mListener.getWebAddress()); if (cookie != null && cookie.length() > 0) { - mHeaders.put("cookie", cookie); + mHeaders.put("Cookie", cookie); } } } diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java index 2a84683..1dbd007 100644 --- a/core/java/android/webkit/JWebCoreJavaBridge.java +++ b/core/java/android/webkit/JWebCoreJavaBridge.java @@ -18,6 +18,7 @@ package android.webkit; import android.os.Handler; import android.os.Message; +import android.security.CertTool; import android.util.Log; final class JWebCoreJavaBridge extends Handler { @@ -186,6 +187,15 @@ final class JWebCoreJavaBridge extends Handler { mHasInstantTimer = false; } + private String[] getKeyStrengthList() { + return CertTool.getInstance().getSupportedKeyStrenghs(); + } + + private String getSignedPublicKey(int index, String challenge, String url) { + // generateKeyPair expects organizations which we don't have. Ignore url. + return CertTool.getInstance().generateKeyPair(index, challenge, null); + } + private native void nativeConstructor(); private native void nativeFinalize(); private native void sharedTimerFired(); diff --git a/core/java/android/webkit/LoadListener.java b/core/java/android/webkit/LoadListener.java index d583eb1..39360cd 100644 --- a/core/java/android/webkit/LoadListener.java +++ b/core/java/android/webkit/LoadListener.java @@ -25,16 +25,16 @@ import android.net.http.HttpAuthHeader; import android.net.http.RequestHandle; import android.net.http.SslCertificate; import android.net.http.SslError; -import android.net.http.SslCertificate; import android.os.Handler; import android.os.Message; +import android.security.CertTool; import android.util.Log; import android.webkit.CacheManager.CacheResult; +import android.widget.Toast; import com.android.internal.R; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -72,6 +72,8 @@ class LoadListener extends Handler implements EventHandler { private static final int HTTP_NOT_FOUND = 404; private static final int HTTP_PROXY_AUTH = 407; + private static final String CERT_MIMETYPE = "application/x-x509-ca-cert"; + private static int sNativeLoaderCount; private final ByteArrayBuilder mDataBuilder = new ByteArrayBuilder(8192); @@ -934,6 +936,12 @@ class LoadListener extends Handler implements EventHandler { // This commits the headers without checking the response status code. private void commitHeaders() { + if (mIsMainPageLoader && CERT_MIMETYPE.equals(mMimeType)) { + // In the case of downloading certificate, we will save it to the + // Keystore in commitLoad. Do not call webcore. + return; + } + // Commit the headers to WebCore int nativeResponse = createNativeResponse(); // The native code deletes the native response object. @@ -974,6 +982,30 @@ class LoadListener extends Handler implements EventHandler { private void commitLoad() { if (mCancelled) return; + if (mIsMainPageLoader && CERT_MIMETYPE.equals(mMimeType)) { + // In the case of downloading certificate, we will save it to the + // Keystore and stop the current loading so that it will not + // generate a new history page + byte[] cert = new byte[mDataBuilder.getByteSize()]; + int position = 0; + ByteArrayBuilder.Chunk c; + while (true) { + c = mDataBuilder.getFirstChunk(); + if (c == null) break; + + if (c.mLength != 0) { + System.arraycopy(c.mArray, 0, cert, position, c.mLength); + position += c.mLength; + } + mDataBuilder.releaseChunk(c); + } + CertTool.getInstance().addCertificate(cert, mContext); + Toast.makeText(mContext, R.string.certificateSaved, + Toast.LENGTH_SHORT).show(); + mBrowserFrame.stopLoading(); + return; + } + // Give the data to WebKit now PerfChecker checker = new PerfChecker(); ByteArrayBuilder.Chunk c; diff --git a/core/java/android/webkit/TextDialog.java b/core/java/android/webkit/TextDialog.java index 9de97c9..99de56d 100644 --- a/core/java/android/webkit/TextDialog.java +++ b/core/java/android/webkit/TextDialog.java @@ -538,7 +538,8 @@ import java.util.ArrayList; * removing the password input type. */ public void setSingleLine(boolean single) { - int inputType = EditorInfo.TYPE_CLASS_TEXT; + int inputType = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; if (!single) { inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 105eacd..ec671d5 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -69,7 +69,24 @@ public class WebSettings { } int value; } - + + /** + * Enum for specifying the WebView's desired density. + * FAR makes 100% looking like in 240dpi + * MEDIUM makes 100% looking like in 160dpi + * CLOSE makes 100% looking like in 120dpi + * @hide Pending API council approval + */ + public enum ZoomDensity { + FAR(150), // 240dpi + MEDIUM(100), // 160dpi + CLOSE(75); // 120dpi + ZoomDensity(int size) { + value = size; + } + int value; + } + /** * Default cache usage pattern Use with {@link #setCacheMode}. */ @@ -105,6 +122,8 @@ public class WebSettings { LOW } + // WebView associated with this WebSettings. + private WebView mWebView; // BrowserFrame used to access the native frame pointer. private BrowserFrame mBrowserFrame; // Flag to prevent multiple SYNC messages at one time. @@ -123,7 +142,7 @@ public class WebSettings { private String mSerifFontFamily = "serif"; private String mCursiveFontFamily = "cursive"; private String mFantasyFontFamily = "fantasy"; - private String mDefaultTextEncoding = "Latin-1"; + private String mDefaultTextEncoding; private String mUserAgent; private boolean mUseDefaultUserAgent; private String mAcceptLanguage; @@ -145,6 +164,7 @@ public class WebSettings { // Don't need to synchronize the get/set methods as they // are basic types, also none of these values are used in // native WebCore code. + private ZoomDensity mDefaultZoom = ZoomDensity.MEDIUM; private RenderPriority mRenderPriority = RenderPriority.NORMAL; private int mOverrideCacheMode = LOAD_DEFAULT; private boolean mSaveFormData = true; @@ -237,9 +257,12 @@ public class WebSettings { * Package constructor to prevent clients from creating a new settings * instance. */ - WebSettings(Context context) { + WebSettings(Context context, WebView webview) { mEventHandler = new EventHandler(); mContext = context; + mWebView = webview; + mDefaultTextEncoding = context.getString(com.android.internal. + R.string.default_text_encoding); if (sLockForLocaleSettings == null) { sLockForLocaleSettings = new Object(); @@ -445,6 +468,31 @@ public class WebSettings { } /** + * Set the default zoom density of the page. This should be called from UI + * thread. + * @param zoom A ZoomDensity value + * @see WebSettings.ZoomDensity + * @hide Pending API council approval + */ + public void setDefaultZoom(ZoomDensity zoom) { + if (mDefaultZoom != zoom) { + mDefaultZoom = zoom; + mWebView.updateDefaultZoomDensity(zoom.value); + } + } + + /** + * Get the default zoom density of the page. This should be called from UI + * thread. + * @return A ZoomDensity value + * @see WebSettings.ZoomDensity + * @hide Pending API council approval + */ + public ZoomDensity getDefaultZoom() { + return mDefaultZoom; + } + + /** * Enables using light touches to make a selection and activate mouseovers. */ public void setLightTouchEnabled(boolean enabled) { diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 563d819..fcf946f 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -237,6 +237,7 @@ public class WebView extends AbsoluteLayout * Helper class to get velocity for fling */ VelocityTracker mVelocityTracker; + private int mMaximumFling; /** * Touch mode @@ -395,22 +396,27 @@ public class WebView extends AbsoluteLayout // width which view is considered to be fully zoomed out static final int ZOOM_OUT_WIDTH = 1008; - private static final float DEFAULT_MAX_ZOOM_SCALE = 4.0f; - private static final float DEFAULT_MIN_ZOOM_SCALE = 0.25f; + // default scale limit. Depending on the display density + private static float DEFAULT_MAX_ZOOM_SCALE; + private static float DEFAULT_MIN_ZOOM_SCALE; // scale limit, which can be set through viewport meta tag in the web page - private float mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; - private float mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; + private float mMaxZoomScale; + private float mMinZoomScale; private boolean mMinZoomScaleFixed = false; // initial scale in percent. 0 means using default. private int mInitialScale = 0; + // default scale. Depending on the display density. + static int DEFAULT_SCALE_PERCENT; + private float mDefaultScale; + // set to true temporarily while the zoom control is being dragged private boolean mPreviewZoomOnly = false; // computed scale and inverse, from mZoomWidth. - private float mActualScale = 1; - private float mInvActualScale = 1; + private float mActualScale; + private float mInvActualScale; // if this is non-zero, it is used on drawing rather than mActualScale private float mZoomScale; private float mInvInitialZoomScale; @@ -635,7 +641,7 @@ public class WebView extends AbsoluteLayout mZoomFitPageButton.setOnClickListener( new View.OnClickListener() { public void onClick(View v) { - zoomWithPreview(1f); + zoomWithPreview(mDefaultScale); updateZoomButtonsEnabled(); } }); @@ -658,7 +664,7 @@ public class WebView extends AbsoluteLayout // or out. mZoomButtonsController.setZoomInEnabled(canZoomIn); mZoomButtonsController.setZoomOutEnabled(canZoomOut); - mZoomFitPageButton.setEnabled(mActualScale != 1); + mZoomFitPageButton.setEnabled(mActualScale != mDefaultScale); } mZoomOverviewButton.setVisibility(canZoomScrollOut() ? View.VISIBLE: View.GONE); @@ -671,13 +677,41 @@ public class WebView extends AbsoluteLayout setClickable(true); setLongClickable(true); - final int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + final int slop = configuration.getScaledTouchSlop(); mTouchSlopSquare = slop * slop; mMinLockSnapReverseDistance = slop; + final float density = getContext().getResources().getDisplayMetrics().density; // use one line height, 16 based on our current default font, for how // far we allow a touch be away from the edge of a link - mNavSlop = (int) (16 * getContext().getResources() - .getDisplayMetrics().density); + mNavSlop = (int) (16 * density); + // density adjusted scale factors + DEFAULT_SCALE_PERCENT = (int) (100 * density); + mDefaultScale = density; + mActualScale = density; + mInvActualScale = 1 / density; + DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; + DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; + mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; + mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; + mMaximumFling = configuration.getScaledMaximumFlingVelocity(); + } + + /* package */void updateDefaultZoomDensity(int zoomDensity) { + final float density = getContext().getResources().getDisplayMetrics().density + * 100 / zoomDensity; + if (Math.abs(density - mDefaultScale) > 0.01) { + float scaleFactor = density / mDefaultScale; + // adjust the limits + mNavSlop = (int) (16 * density); + DEFAULT_SCALE_PERCENT = (int) (100 * density); + DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; + DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; + mDefaultScale = density; + mMaxZoomScale *= scaleFactor; + mMinZoomScale *= scaleFactor; + setNewZoomScale(mActualScale * scaleFactor, false); + } } /* package */ boolean onSavePassword(String schemePlusHost, String username, @@ -1118,6 +1152,29 @@ public class WebView extends AbsoluteLayout } /** + * Load the url with postData using "POST" method into the WebView. If url + * is not a network url, it will be loaded with {link + * {@link #loadUrl(String)} instead. + * + * @param url The url of the resource to load. + * @param postData The data will be passed to "POST" request. + * + * @hide pending API solidification + */ + public void postUrl(String url, byte[] postData) { + if (URLUtil.isNetworkUrl(url)) { + switchOutDrawHistory(); + HashMap arg = new HashMap(); + arg.put("url", url); + arg.put("data", postData); + mWebViewCore.sendMessage(EventHub.POST_URL, arg); + clearTextEntry(); + } else { + loadUrl(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. @@ -4103,7 +4160,7 @@ public class WebView extends AbsoluteLayout int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0); int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0); - mVelocityTracker.computeCurrentVelocity(1000); + mVelocityTracker.computeCurrentVelocity(1000, mMaximumFling); int vx = (int) mVelocityTracker.getXVelocity(); int vy = (int) mVelocityTracker.getYVelocity(); @@ -4134,9 +4191,9 @@ public class WebView extends AbsoluteLayout private boolean zoomWithPreview(float scale) { float oldScale = mActualScale; - // snap to 100% if it is close - if (scale > 0.95f && scale < 1.05f) { - scale = 1.0f; + // snap to DEFAULT_SCALE if it is close + if (scale > (mDefaultScale - 0.05) && scale < (mDefaultScale + 0.05)) { + scale = mDefaultScale; } setNewZoomScale(scale, false); @@ -4517,9 +4574,11 @@ public class WebView extends AbsoluteLayout break; } case SWITCH_TO_LONGPRESS: { - mTouchMode = TOUCH_DONE_MODE; - performLongClick(); - updateTextEntry(); + if (!mPreventDrag) { + mTouchMode = TOUCH_DONE_MODE; + performLongClick(); + updateTextEntry(); + } break; } case SWITCH_TO_ENTER: @@ -4651,8 +4710,8 @@ public class WebView extends AbsoluteLayout } int initialScale = msg.arg1; int viewportWidth = msg.arg2; - // by default starting a new page with 100% zoom scale. - float scale = 1.0f; + // start a new page with DEFAULT_SCALE zoom scale. + float scale = mDefaultScale; if (mInitialScale > 0) { scale = mInitialScale / 100.0f; } else { diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index e9df453..a5fa41e 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -97,7 +97,7 @@ final class WebViewCore { private boolean mViewportUserScalable = true; - private int mRestoredScale = 100; + private int mRestoredScale = WebView.DEFAULT_SCALE_PERCENT; private int mRestoredX = 0; private int mRestoredY = 0; @@ -139,7 +139,7 @@ final class WebViewCore { // ready. mEventHub = new EventHub(); // Create a WebSettings object for maintaining all settings - mSettings = new WebSettings(mContext); + mSettings = new WebSettings(mContext, mWebView); // The WebIconDatabase needs to be initialized within the UI thread so // just request the instance here. WebIconDatabase.getInstance(); @@ -544,6 +544,8 @@ final class WebViewCore { "WEBKIT_DRAW", // = 130; "SYNC_SCROLL", // = 131; "REFRESH_PLUGINS", // = 132; + // this will replace REFRESH_PLUGINS in the next release + "POST_URL", // = 142; "SPLIT_PICTURE_SET", // = 133; "CLEAR_CONTENT", // = 134; "SET_FINAL_FOCUS", // = 135; @@ -589,6 +591,8 @@ final class WebViewCore { static final int WEBKIT_DRAW = 130; static final int SYNC_SCROLL = 131; static final int REFRESH_PLUGINS = 132; + // this will replace REFRESH_PLUGINS in the next release + static final int POST_URL = 142; static final int SPLIT_PICTURE_SET = 133; static final int CLEAR_CONTENT = 134; @@ -672,6 +676,13 @@ final class WebViewCore { loadUrl((String) msg.obj); break; + case POST_URL: { + HashMap param = (HashMap) msg.obj; + String url = (String) param.get("url"); + byte[] data = (byte[]) param.get("data"); + mBrowserFrame.postUrl(url, data); + break; + } case LOAD_DATA: HashMap loadParams = (HashMap) msg.obj; String baseUrl = (String) loadParams.get("baseUrl"); @@ -1549,19 +1560,33 @@ final class WebViewCore { // set the viewport settings from WebKit setViewportSettingsFromNative(); + // adjust the default scale to match the density + if (WebView.DEFAULT_SCALE_PERCENT != 100) { + float adjust = (float) WebView.DEFAULT_SCALE_PERCENT / 100.0f; + if (mViewportInitialScale > 0) { + mViewportInitialScale *= adjust; + } + if (mViewportMinimumScale > 0) { + mViewportMinimumScale *= adjust; + } + if (mViewportMaximumScale > 0) { + mViewportMaximumScale *= adjust; + } + } + // infer the values if they are not defined. if (mViewportWidth == 0) { if (mViewportInitialScale == 0) { - mViewportInitialScale = 100; + mViewportInitialScale = WebView.DEFAULT_SCALE_PERCENT; } if (mViewportMinimumScale == 0) { - mViewportMinimumScale = 100; + mViewportMinimumScale = WebView.DEFAULT_SCALE_PERCENT; } } if (mViewportUserScalable == false) { - mViewportInitialScale = 100; - mViewportMinimumScale = 100; - mViewportMaximumScale = 100; + mViewportInitialScale = WebView.DEFAULT_SCALE_PERCENT; + mViewportMinimumScale = WebView.DEFAULT_SCALE_PERCENT; + mViewportMaximumScale = WebView.DEFAULT_SCALE_PERCENT; } if (mViewportMinimumScale > mViewportInitialScale) { if (mViewportInitialScale == 0) { @@ -1575,9 +1600,10 @@ final class WebViewCore { mViewportMaximumScale = mViewportInitialScale; } else if (mViewportInitialScale == 0) { mViewportInitialScale = mViewportMaximumScale; - } + } } - if (mViewportWidth < 0 && mViewportInitialScale == 100) { + if (mViewportWidth < 0 + && mViewportInitialScale == WebView.DEFAULT_SCALE_PERCENT) { mViewportWidth = 0; } diff --git a/core/java/android/webkit/gears/AndroidRadioDataProvider.java b/core/java/android/webkit/gears/AndroidRadioDataProvider.java index 2d431a8..1384042 100644 --- a/core/java/android/webkit/gears/AndroidRadioDataProvider.java +++ b/core/java/android/webkit/gears/AndroidRadioDataProvider.java @@ -28,6 +28,7 @@ package android.webkit.gears; import android.content.Context; import android.telephony.CellLocation; import android.telephony.ServiceState; +import android.telephony.SignalStrength; import android.telephony.gsm.GsmCellLocation; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; @@ -54,6 +55,7 @@ public final class AndroidRadioDataProvider extends PhoneStateListener { public static final class RadioData { public int cellId = -1; public int locationAreaCode = -1; + // TODO: use new SignalStrength instead of asu public int signalStrength = -1; public int mobileCountryCode = -1; public int mobileNetworkCode = -1; @@ -179,6 +181,7 @@ public final class AndroidRadioDataProvider extends PhoneStateListener { private CellLocation cellLocation = null; /** The last known signal strength */ + // TODO: use new SignalStrength instead of asu private int signalStrength = -1; /** The last known serviceState */ @@ -207,7 +210,7 @@ public final class AndroidRadioDataProvider extends PhoneStateListener { // Register for cell id, signal strength and service state changed // notifications. telephonyManager.listen(this, PhoneStateListener.LISTEN_CELL_LOCATION - | PhoneStateListener.LISTEN_SIGNAL_STRENGTH + | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS | PhoneStateListener.LISTEN_SERVICE_STATE); } @@ -226,8 +229,9 @@ public final class AndroidRadioDataProvider extends PhoneStateListener { } @Override - public void onSignalStrengthChanged(int asu) { - signalStrength = asu; + public void onSignalStrengthsChanged(SignalStrength ss) { + int gsmSignalStrength = ss.getGsmSignalStrength(); + signalStrength = (gsmSignalStrength == 99 ? -1 : gsmSignalStrength); notifyListeners(); } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 1ca59b2..f9ca8cb 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -54,7 +54,9 @@ import java.util.ArrayList; import java.util.List; /** - * Common code shared between ListView and GridView + * Base class that can be used to implement virtualized lists of items. A list does + * not have a spatial definition here. For instance, subclases of this class can + * display the content of the list in a grid, in a carousel, as stack, etc. * * @attr ref android.R.styleable#AbsListView_listSelector * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop @@ -86,7 +88,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public static final int TRANSCRIPT_MODE_NORMAL = 1; /** * The list will automatically scroll to the bottom, no matter what items - * are currently visible. + * are currently visible. * * @see #setTranscriptMode(int) */ @@ -123,7 +125,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * Indicates the view is in the process of being flung */ static final int TOUCH_MODE_FLING = 4; - + /** * Indicates that the user is currently dragging the fast scroll thumb */ @@ -316,7 +318,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * bitmap cache after scrolling. */ boolean mScrollingCacheEnabled; - + /** * Whether or not to enable the fast scroll feature on this list */ @@ -389,7 +391,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * The last CheckForTap runnable we posted, if any */ private Runnable mPendingCheckForTap; - + /** * The last CheckForKeyLongPress runnable we posted, if any */ @@ -427,14 +429,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ private FastScroller mFastScroller; - private int mTouchSlop; + private boolean mGlobalLayoutListenerAddedFilter; + private int mTouchSlop; private float mDensityScale; private InputConnection mDefInputConnection; private InputConnectionWrapper mPublicInputConnection; private Runnable mClearScrollingCache; + private int mMinimumVelocity; + private int mMaximumVelocity; /** * Interface definition for a callback to be invoked when the list or grid @@ -529,21 +534,35 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te int color = a.getColor(R.styleable.AbsListView_cacheColorHint, 0); setCacheColorHint(color); - + boolean enableFastScroll = a.getBoolean(R.styleable.AbsListView_fastScrollEnabled, false); setFastScrollEnabled(enableFastScroll); boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true); setSmoothScrollbarEnabled(smoothScrollbar); - + a.recycle(); } + private void initAbsListView() { + // Setting focusable in touch mode will set the focusable property to true + setFocusableInTouchMode(true); + setWillNotDraw(false); + setAlwaysDrawnWithCacheEnabled(false); + setScrollingCacheEnabled(true); + + final ViewConfiguration configuration = ViewConfiguration.get(mContext); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mDensityScale = getContext().getResources().getDisplayMetrics().density; + } + /** - * Enables fast scrolling by letting the user quickly scroll through lists by - * dragging the fast scroll thumb. The adapter attached to the list may want + * Enables fast scrolling by letting the user quickly scroll through lists by + * dragging the fast scroll thumb. The adapter attached to the list may want * to implement {@link SectionIndexer} if it wishes to display alphabet preview and - * jump between sections of the list. + * jump between sections of the list. * @see SectionIndexer * @see #isFastScrollEnabled() * @param enabled whether or not to enable fast scrolling @@ -561,7 +580,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } } - + /** * Returns the current state of the fast scroll feature. * @see #setFastScrollEnabled(boolean) @@ -571,10 +590,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public boolean isFastScrollEnabled() { return mFastScrollEnabled; } - + /** * If fast scroll is visible, then don't draw the vertical scrollbar. - * @hide + * @hide */ @Override protected boolean isVerticalScrollBarHidden() { @@ -592,11 +611,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * When smooth scrollbar is disabled, the position and size of the scrollbar thumb * is based solely on the number of items in the adapter and the position of the * visible items inside the adapter. This provides a stable scrollbar as the user - * navigates through a list of items with varying heights. + * navigates through a list of items with varying heights. * * @param enabled Whether or not to enable smooth scrollbar. * - * @see #setSmoothScrollbarEnabled(boolean) + * @see #setSmoothScrollbarEnabled(boolean) * @attr ref android.R.styleable#AbsListView_smoothScrollbar */ public void setSmoothScrollbarEnabled(boolean enabled) { @@ -712,17 +731,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } - private void initAbsListView() { - // Setting focusable in touch mode will set the focusable property to true - setFocusableInTouchMode(true); - setWillNotDraw(false); - setAlwaysDrawnWithCacheEnabled(false); - setScrollingCacheEnabled(true); - - mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); - mDensityScale = getContext().getResources().getDisplayMetrics().density; - } - private void useDefaultSelector() { setSelector(getResources().getDrawable( com.android.internal.R.drawable.list_selector_background)); @@ -828,7 +836,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public Parcelable onSaveInstanceState() { /* * This doesn't really make sense as the place to dismiss the - * popup, but there don't seem to be any other useful hooks + * popups, but there don't seem to be any other useful hooks * that happen early enough to keep from getting complaints * about having leaked the window. */ @@ -908,17 +916,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } private boolean acceptFilter() { - if (!mTextFilterEnabled || !(getAdapter() instanceof Filterable) || - ((Filterable) getAdapter()).getFilter() == null) { - return false; - } - return true; + return mTextFilterEnabled && getAdapter() instanceof Filterable && + ((Filterable) getAdapter()).getFilter() != null; } /** * Sets the initial value for the text filter. * @param filterText The text to use for the filter. - * + * * @see #setTextFilterEnabled */ public void setFilterText(String filterText) { @@ -944,7 +949,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } /** - * Returns the list's text filter, if available. + * Returns the list's text filter, if available. * @return the list's text filter or null if filtering isn't enabled */ public CharSequence getTextFilter() { @@ -953,7 +958,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } return null; } - + @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); @@ -1096,6 +1101,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te listPadding.bottom = mSelectionBottomPadding + mPaddingBottom; } + /** + * Subclasses should NOT override this method but + * {@link #layoutChildren()} instead. + */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); @@ -1111,17 +1120,22 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te protected boolean setFrame(int left, int top, int right, int bottom) { final boolean changed = super.setFrame(left, top, right, bottom); - // Reposition the popup when the frame has changed. This includes - // translating the widget, not just changing its dimension. The - // filter popup needs to follow the widget. - if (mFiltered && changed && getWindowVisibility() == View.VISIBLE && mPopup != null && - mPopup.isShowing()) { - positionPopup(); + if (changed) { + // Reposition the popup when the frame has changed. This includes + // translating the widget, not just changing its dimension. The + // filter popup needs to follow the widget. + final boolean visible = getWindowVisibility() == View.VISIBLE; + if (mFiltered && visible && mPopup != null && mPopup.isShowing()) { + positionPopup(); + } } return changed; } + /** + * Subclasses must override this method to layout their children. + */ protected void layoutChildren() { } @@ -1324,6 +1338,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mDataChanged = true; rememberSyncState(); } + if (mFastScroller != null) { mFastScroller.onSizeChanged(w, h, oldw, oldh); } @@ -1494,7 +1509,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te System.arraycopy(state, enabledPos + 1, state, enabledPos, state.length - enabledPos - 1); } - + return state; } @@ -1510,6 +1525,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final ViewTreeObserver treeObserver = getViewTreeObserver(); if (treeObserver != null) { treeObserver.addOnTouchModeChangeListener(this); + if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) { + treeObserver.addOnGlobalLayoutListener(this); + } } } @@ -1520,6 +1538,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final ViewTreeObserver treeObserver = getViewTreeObserver(); if (treeObserver != null) { treeObserver.removeOnTouchModeChangeListener(this); + if (mTextFilterEnabled && mPopup != null) { + treeObserver.removeGlobalOnLayoutListener(this); + mGlobalLayoutListenerAddedFilter = false; + } } } @@ -1586,16 +1608,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te */ private class WindowRunnnable { private int mOriginalAttachCount; - + public void rememberWindowAttachCount() { mOriginalAttachCount = getWindowAttachCount(); } - + public boolean sameWindow() { return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount; } } - + private class PerformClick extends WindowRunnnable implements Runnable { View mChild; int mClickMotionPosition; @@ -1622,7 +1644,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final long longPressId = mAdapter.getItemId(mMotionPosition); boolean handled = false; - if (sameWindow() && !mDataChanged) { + if (sameWindow() && !mDataChanged) { handled = performLongPress(child, longPressPosition, longPressId); } if (handled) { @@ -1636,7 +1658,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } } - + private class CheckForKeyLongPress extends WindowRunnnable implements Runnable { public void run() { if (isPressed() && mSelectedPosition >= 0) { @@ -1812,7 +1834,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mTouchMode = TOUCH_MODE_DONE_WAITING; } } else { - mTouchMode = TOUCH_MODE_DONE_WAITING; + mTouchMode = TOUCH_MODE_DONE_WAITING; } } } @@ -1867,13 +1889,13 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override public boolean onTouchEvent(MotionEvent ev) { - if (mFastScroller != null) { boolean intercepted = mFastScroller.onTouchEvent(ev); if (intercepted) { return true; - } + } } + final int action = ev.getAction(); final int x = (int) ev.getX(); final int y = (int) ev.getY(); @@ -2041,12 +2063,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te break; case TOUCH_MODE_SCROLL: final VelocityTracker velocityTracker = mVelocityTracker; - velocityTracker.computeCurrentVelocity(1000); - int initialVelocity = (int)velocityTracker.getYVelocity(); - - if ((Math.abs(initialVelocity) > - ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity()) && - (getChildCount() > 0)) { + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + final int initialVelocity = (int) velocityTracker.getYVelocity(); + if (Math.abs(initialVelocity) > mMinimumVelocity && (getChildCount() > 0)) { if (mFlingRunnable == null) { mFlingRunnable = new FlingRunnable(); } @@ -2059,10 +2078,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } setPressed(false); - + // Need to redraw since we probably aren't drawing the selector anymore invalidate(); - + final Handler handler = getHandler(); if (handler != null) { handler.removeCallbacks(mPendingCheckForLongPress); @@ -2106,7 +2125,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return true; } - + @Override public void draw(Canvas canvas) { super.draw(canvas); @@ -2121,14 +2140,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te int x = (int) ev.getX(); int y = (int) ev.getY(); View v; - + if (mFastScroller != null) { boolean intercepted = mFastScroller.onInterceptTouchEvent(ev); if (intercepted) { return true; } } - + switch (action) { case MotionEvent.ACTION_DOWN: { int motionPosition = findMotionRow(y); @@ -2775,7 +2794,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te /** * Removes the filter window */ - void dismissPopup() { + private void dismissPopup() { if (mPopup != null) { mPopup.dismiss(); } @@ -2978,7 +2997,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } return null; } - + /** * For filtering we proxy an input connection to an internal text editor, * and this allows the proxying to happen. @@ -2987,7 +3006,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te public boolean checkInputConnectionProxy(View view) { return view == mTextFilter; } - + /** * Creates the window for the text filter and populates it with an EditText field; * @@ -3017,6 +3036,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te p.setBackgroundDrawable(null); mPopup = p; getViewTreeObserver().addOnGlobalLayoutListener(this); + mGlobalLayoutListenerAddedFilter = true; } if (animateEntrance) { mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter); @@ -3379,7 +3399,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mCurrentScrap = scrapViews[0]; mScrapViews = scrapViews; } - + public boolean shouldRecycleViewType(int viewType) { return viewType >= 0; } diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index 173e80f..7d2fcbc 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -24,11 +24,12 @@ import android.os.SystemClock; import android.util.AttributeSet; import android.util.SparseArray; import android.view.ContextMenu; +import android.view.SoundEffectConstants; import android.view.View; -import android.view.ViewGroup; import android.view.ViewDebug; -import android.view.SoundEffectConstants; +import android.view.ViewGroup; import android.view.ContextMenu.ContextMenuInfo; +import android.view.accessibility.AccessibilityEvent; /** @@ -618,7 +619,9 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { } /** - * Sets the currently selected item + * Sets the currently selected item. To support accessibility subclasses that + * override this method must invoke the overriden super method first. + * * @param position Index (starting at 0) of the data item to be selected. */ public abstract void setSelection(int position); @@ -844,6 +847,11 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { fireOnSelected(); } } + + // we fire selection events here not in View + if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) { + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } } private void fireOnSelected() { @@ -861,6 +869,35 @@ 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 + // events so we change the event type here. + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) { + event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + + // 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); + } + + if (!populated) { + if (selectedView != null) { + event.setEnabled(selectedView.isEnabled()); + } + event.setItemCount(getCount()); + event.setCurrentItemIndex(getSelectedItemPosition()); + } + + return populated; + } + + @Override protected boolean canAnimate() { return super.canAnimate() && mItemCount > 0; } diff --git a/core/java/android/widget/AlphabetIndexer.java b/core/java/android/widget/AlphabetIndexer.java index 4e466a0..f50676a 100644 --- a/core/java/android/widget/AlphabetIndexer.java +++ b/core/java/android/widget/AlphabetIndexer.java @@ -248,8 +248,8 @@ public class AlphabetIndexer extends DataSetObserver implements SectionIndexer { public int getSectionForPosition(int position) { int savedCursorPos = mDataCursor.getPosition(); mDataCursor.moveToPosition(position); - mDataCursor.moveToPosition(savedCursorPos); String curName = mDataCursor.getString(mColumnIndex); + mDataCursor.moveToPosition(savedCursorPos); // Linear search, as there are only a few items in the section index // Could speed this up later if it actually gets used. for (int i = 0; i < mAlphabetLength; i++) { diff --git a/core/java/android/widget/AppSecurityPermissions.java b/core/java/android/widget/AppSecurityPermissions.java index 5fa00e7..c4b5ef8 100755 --- a/core/java/android/widget/AppSecurityPermissions.java +++ b/core/java/android/widget/AppSecurityPermissions.java @@ -124,25 +124,25 @@ public class AppSecurityPermissions implements View.OnClickListener { if(pkg == null) { return; } - // Extract shared user permissions if any + // Get requested permissions + if (pkg.requestedPermissions != null) { + ArrayList<String> strList = pkg.requestedPermissions; + int size = strList.size(); + if (size > 0) { + extractPerms(strList.toArray(new String[size]), permSet); + } + } + // Get permissions related to shared user if any if(pkg.mSharedUserId != null) { int sharedUid; try { sharedUid = mPm.getUidForSharedUser(pkg.mSharedUserId); + getAllUsedPermissions(sharedUid, permSet); } catch (NameNotFoundException e) { Log.w(TAG, "Could'nt retrieve shared user id for:"+pkg.packageName); - return; } - getAllUsedPermissions(sharedUid, permSet); - } else { - ArrayList<String> strList = pkg.requestedPermissions; - int size; - if((strList == null) || ((size = strList.size()) == 0)) { - return; - } - // Extract permissions defined in current package - extractPerms(strList.toArray(new String[size]), permSet); } + // Retrieve list of permissions for(PermissionInfo tmpInfo : permSet) { mPermsList.add(tmpInfo); } @@ -176,14 +176,9 @@ public class AppSecurityPermissions implements View.OnClickListener { Log.w(TAG, "Could'nt retrieve permissions for package:"+packageName); return; } - if(pkgInfo == null) { - return; - } - String strList[] = pkgInfo.requestedPermissions; - if(strList == null) { - return; + if ((pkgInfo != null) && (pkgInfo.requestedPermissions != null)) { + extractPerms(pkgInfo.requestedPermissions, permSet); } - extractPerms(strList, permSet); } private void extractPerms(String strList[], Set<PermissionInfo> permSet) { diff --git a/core/java/android/widget/ArrayAdapter.java b/core/java/android/widget/ArrayAdapter.java index c28210d..32e5504 100644 --- a/core/java/android/widget/ArrayAdapter.java +++ b/core/java/android/widget/ArrayAdapter.java @@ -348,7 +348,12 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { "ArrayAdapter requires the resource ID to be a TextView", e); } - text.setText(getItem(position).toString()); + T item = getItem(position); + if (item instanceof CharSequence) { + text.setText((CharSequence)item); + } else { + text.setText(item.toString()); + } return view; } diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index a1d16ea..675aba2 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -80,6 +80,7 @@ import com.android.internal.R; * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight */ public class AutoCompleteTextView extends EditText implements Filter.FilterListener { static final boolean DEBUG = false; @@ -101,6 +102,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private int mDropDownAnchorId; private View mDropDownAnchorView; // view is retrieved lazily from id once needed private int mDropDownWidth; + private int mDropDownHeight; private Drawable mDropDownListHighlight; @@ -122,10 +124,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private boolean mBlockCompletion; private AutoCompleteTextView.ListSelectorHider mHideSelector; - - // Indicates whether this AutoCompleteTextView is attached to a window or not - // The widget is attached to a window when mAttachCount > 0 - private int mAttachCount; + private Runnable mShowDropDownRunnable; private AutoCompleteTextView.PassThroughClickListener mPassThroughClickListener; @@ -170,6 +169,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view). mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth, ViewGroup.LayoutParams.WRAP_CONTENT); + mDropDownHeight = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownHeight, + ViewGroup.LayoutParams.WRAP_CONTENT); mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView, R.layout.simple_dropdown_hint); @@ -258,6 +259,34 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public void setDropDownWidth(int width) { mDropDownWidth = width; } + + /** + * <p>Returns the current height for the auto-complete drop down list. This can + * be a fixed height, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill + * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height + * of the drop down's content.</p> + * + * @return the height for the drop down list + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight + */ + public int getDropDownHeight() { + return mDropDownHeight; + } + + /** + * <p>Sets the current height for the auto-complete drop down list. This can + * be a fixed height, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill + * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height + * of the drop down's content.</p> + * + * @param height the height to use + * + * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight + */ + public void setDropDownHeight(int height) { + mDropDownHeight = height; + } /** * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p> @@ -589,7 +618,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe if (isPopupShowing()) { // special case for the back key, we do not even try to send it // to the drop down list but instead, consume it immediately - if (keyCode == KeyEvent.KEYCODE_BACK) { + if (keyCode == KeyEvent.KEYCODE_BACK && !mDropDownAlwaysVisible) { dismissDropDown(); return true; } @@ -637,15 +666,19 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mDropDownList.getAdapter().getCount() - 1)) { // When the selection is at the top, we block the key // event to prevent focus from moving. - mDropDownList.hideSelector(); - mDropDownList.requestLayout(); + clearListSelection(); mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); - mPopup.update(); + showDropDown(); return true; + } else { + // WARNING: Please read the comment where mListSelectionHidden + // is declared + mDropDownList.mListSelectionHidden = false; } + consumed = mDropDownList.onKeyDown(keyCode, event); - if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" - + consumed); + if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed); + if (consumed) { // If it handled the key event, then the user is // navigating in the list, so we should put it in front. @@ -655,7 +688,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe // by ensuring it has focus and getting its window out // of touch mode. mDropDownList.requestFocusFromTouch(); - mPopup.update(); + showDropDown(); switch (keyCode) { // avoid passing the focus from the text view to the @@ -755,7 +788,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } else { // drop down is automatically dismissed when enough characters // are deleted from the text view - dismissDropDown(); + if (!mDropDownAlwaysVisible) dismissDropDown(); if (mFilter != null) { mFilter.filter(null); } @@ -788,9 +821,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * it back. */ public void clearListSelection() { - if (mDropDownList != null) { - mDropDownList.hideSelector(); - mDropDownList.requestLayout(); + final DropDownListView list = mDropDownList; + if (list != null) { + // WARNING: Please read the comment where mListSelectionHidden is declared + list.mListSelectionHidden = true; + list.hideSelector(); + list.requestLayout(); } } @@ -801,6 +837,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe */ public void setListSelection(int position) { if (mPopup.isShowing() && (mDropDownList != null)) { + mDropDownList.mListSelectionHidden = false; mDropDownList.setSelection(position); // ListView.setSelection() will call requestLayout() } @@ -893,7 +930,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } } - if (mDropDownDismissedOnCompletion) { + if (mDropDownDismissedOnCompletion && !mDropDownAlwaysVisible) { dismissDropDown(); } } @@ -950,6 +987,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @param text the selected suggestion in the drop down list */ protected void replaceText(CharSequence text) { + clearComposingText(); + setText(text); // make sure we keep the caret at the end of the text view Editable spannable = getText(); @@ -958,7 +997,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe /** {@inheritDoc} */ public void onFilterComplete(int count) { - if (mAttachCount <= 0) return; + // Not attached to window, don't update drop-down + if (getWindowVisibility() == View.GONE) return; /* * This checks enoughToFilter() again because filtering requests @@ -971,7 +1011,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe if (hasFocus() && hasWindowFocus()) { showDropDown(); } - } else { + } else if (!mDropDownAlwaysVisible) { dismissDropDown(); } } @@ -980,7 +1020,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); performValidation(); - if (!hasWindowFocus) { + if (!hasWindowFocus && !mDropDownAlwaysVisible) { dismissDropDown(); } } @@ -989,7 +1029,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(focused, direction, previouslyFocusedRect); performValidation(); - if (!focused) { + if (!focused && !mDropDownAlwaysVisible) { dismissDropDown(); } } @@ -997,13 +1037,11 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - mAttachCount++; } @Override protected void onDetachedFromWindow() { dismissDropDown(); - mAttachCount--; super.onDetachedFromWindow(); } @@ -1044,12 +1082,26 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } /** + * Issues a runnable to show the dropdown as soon as possible. + * + * @hide internal used only by Search Dialog + */ + public void showDropDownAfterLayout() { + post(mShowDropDownRunnable); + } + + /** * <p>Displays the drop down on screen.</p> */ public void showDropDown() { int height = buildDropDown(); + + int widthSpec = 0; + int heightSpec = 0; + + boolean noInputMethod = mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED; + if (mPopup.isShowing()) { - int widthSpec; if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { // The call to PopupWindow's update method below can accept -1 for any // value you do not want to update. @@ -1059,20 +1111,51 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } else { widthSpec = mDropDownWidth; } + + if (mDropDownHeight == ViewGroup.LayoutParams.FILL_PARENT) { + // The call to PopupWindow's update method below can accept -1 for any + // value you do not want to update. + heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.FILL_PARENT; + if (noInputMethod) { + mPopup.setWindowLayoutMode( + mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT ? + ViewGroup.LayoutParams.FILL_PARENT : 0, 0); + } else { + mPopup.setWindowLayoutMode( + mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT ? + ViewGroup.LayoutParams.FILL_PARENT : 0, + ViewGroup.LayoutParams.FILL_PARENT); + } + } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + heightSpec = height; + } else { + heightSpec = mDropDownHeight; + } + mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset, - mDropDownVerticalOffset, widthSpec, height); + mDropDownVerticalOffset, widthSpec, heightSpec); } else { if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { - mPopup.setWindowLayoutMode(ViewGroup.LayoutParams.FILL_PARENT, 0); + widthSpec = ViewGroup.LayoutParams.FILL_PARENT; } else { - mPopup.setWindowLayoutMode(0, 0); if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { mPopup.setWidth(getDropDownAnchorView().getWidth()); } else { mPopup.setWidth(mDropDownWidth); } } - mPopup.setHeight(height); + + if (mDropDownHeight == ViewGroup.LayoutParams.FILL_PARENT) { + heightSpec = ViewGroup.LayoutParams.FILL_PARENT; + } else { + if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { + mPopup.setHeight(height); + } else { + mPopup.setHeight(mDropDownHeight); + } + } + + mPopup.setWindowLayoutMode(widthSpec, heightSpec); mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); // use outside touchable to dismiss drop down when touching outside of it, so @@ -1082,8 +1165,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mPopup.showAsDropDown(getDropDownAnchorView(), mDropDownHorizontalOffset, mDropDownVerticalOffset); mDropDownList.setSelection(ListView.INVALID_POSITION); - mDropDownList.hideSelector(); - mDropDownList.requestFocus(); + clearListSelection(); post(mHideSelector); } } @@ -1119,6 +1201,22 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mHideSelector = new ListSelectorHider(); + /** + * This Runnable exists for the sole purpose of checking if the view layout has got + * completed and if so call showDropDown to display the drop down. This is used to show + * the drop down as soon as possible after user opens up the search dialog, without + * waiting for the normal UI pipeline to do it's job which is slower than this method. + */ + mShowDropDownRunnable = new Runnable() { + public void run() { + // View layout should be all done before displaying the drop down. + View view = getDropDownAnchorView(); + if (view != null && view.getWindowToken() != null) { + showDropDown(); + } + } + }; + mDropDownList = new DropDownListView(context); mDropDownList.setSelector(mDropDownListHighlight); mDropDownList.setAdapter(mAdapter); @@ -1126,6 +1224,22 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mDropDownList.setOnItemClickListener(mDropDownItemClickListener); mDropDownList.setFocusable(true); mDropDownList.setFocusableInTouchMode(true); + mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView<?> parent, View view, + int position, long id) { + + if (position != -1) { + DropDownListView dropDownList = mDropDownList; + + if (dropDownList != null) { + dropDownList.mListSelectionHidden = false; + } + } + } + + public void onNothingSelected(AdapterView<?> parent) { + } + }); if (mItemSelectedListener != null) { mDropDownList.setOnItemSelectedListener(mItemSelectedListener); @@ -1180,10 +1294,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe final int maxHeight = mPopup.getMaxAvailableHeight( getDropDownAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); - final int measuredHeight = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, - 0, ListView.NO_POSITION, maxHeight - otherHeights, 2) + otherHeights; + if (mDropDownAlwaysVisible) { + return maxHeight; + } - return mDropDownAlwaysVisible ? maxHeight : measuredHeight; + return mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED, + 0, ListView.NO_POSITION, maxHeight - otherHeights, 2) + otherHeights; } private View getHintView(Context context) { @@ -1249,10 +1365,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe private class ListSelectorHider implements Runnable { public void run() { - if (mDropDownList != null) { - mDropDownList.hideSelector(); - mDropDownList.requestLayout(); - } + clearListSelection(); } } @@ -1279,6 +1392,36 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * passed to the drop down; the list only looks focused.</p> */ private static class DropDownListView extends ListView { + /* + * WARNING: This is a workaround for a touch mode issue. + * + * Touch mode is propagated lazily to windows. This causes problems in + * the following scenario: + * - Type something in the AutoCompleteTextView and get some results + * - Move down with the d-pad to select an item in the list + * - Move up with the d-pad until the selection disappears + * - Type more text in the AutoCompleteTextView *using the soft keyboard* + * and get new results; you are now in touch mode + * - The selection comes back on the first item in the list, even though + * the list is supposed to be in touch mode + * + * Using the soft keyboard triggers the touch mode change but that change + * is propagated to our window only after the first list layout, therefore + * after the list attempts to resurrect the selection. + * + * The trick to work around this issue is to pretend the list is in touch + * mode when we know that the selection should not appear, that is when + * we know the user moved the selection away from the list. + * + * This boolean is set to true whenever we explicitely hide the list's + * selection and reset to false whenver we know the user moved the + * selection back to the list. + * + * When this boolean is true, isInTouchMode() returns true, otherwise it + * returns super.isInTouchMode(). + */ + private boolean mListSelectionHidden; + /** * <p>Creates a new list view wrapper.</p> * @@ -1324,6 +1467,12 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe return mSelectionBottomPadding; } + @Override + public boolean isInTouchMode() { + // WARNING: Please read the comment where mListSelectionHidden is declared + return mListSelectionHidden || super.isInTouchMode(); + } + /** * <p>Returns the focus state in the drop down.</p> * diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index abcc715..fd590ed 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -16,14 +16,15 @@ package android.widget; +import com.android.internal.R; + import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.Gravity; - -import com.android.internal.R; +import android.view.accessibility.AccessibilityEvent; /** @@ -194,5 +195,13 @@ public class CheckedTextView extends TextView implements Checkable { invalidate(); } } - + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + boolean populated = super.dispatchPopulateAccessibilityEvent(event); + if (!populated) { + event.setChecked(mChecked); + } + return populated; + } } diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index d4482dc..98b0976 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -26,7 +26,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.Gravity; - +import android.view.accessibility.AccessibilityEvent; /** * <p> @@ -124,6 +124,7 @@ public abstract class CompoundButton extends Button implements Checkable { if (mOnCheckedChangeWidgetListener != null) { mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked); } + mBroadcasting = false; } } @@ -205,6 +206,25 @@ 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; + } + + @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index 0fc8f49..5360621 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -1083,6 +1083,11 @@ public class ExpandableListView extends ListView { @Override public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); diff --git a/core/java/android/widget/FastScroller.java b/core/java/android/widget/FastScroller.java index 3368477..cd965fc 100644 --- a/core/java/android/widget/FastScroller.java +++ b/core/java/android/widget/FastScroller.java @@ -134,7 +134,7 @@ class FastScroller { mScrollCompleted = true; - getSections(); + getSectionsFromIndexer(); mOverlayPos = new RectF(); mScrollFade = new ScrollFade(); @@ -250,7 +250,18 @@ class FastScroller { } } - private void getSections() { + SectionIndexer getSectionIndexer() { + return mSectionIndexer; + } + + Object[] getSections() { + if (mListAdapter == null && mList != null) { + getSectionsFromIndexer(); + } + return mSections; + } + + private void getSectionsFromIndexer() { Adapter adapter = mList.getAdapter(); mSectionIndexer = null; if (adapter instanceof HeaderViewListAdapter) { @@ -391,8 +402,7 @@ class FastScroller { boolean onInterceptTouchEvent(MotionEvent ev) { if (mState > STATE_NONE && ev.getAction() == MotionEvent.ACTION_DOWN) { - if (ev.getX() > mList.getWidth() - mThumbW && ev.getY() >= mThumbY && - ev.getY() <= mThumbY + mThumbH) { + if (isPointInside(ev.getX(), ev.getY())) { setState(STATE_DRAGGING); return true; } @@ -404,20 +414,20 @@ class FastScroller { if (mState == STATE_NONE) { return false; } - if (me.getAction() == MotionEvent.ACTION_DOWN) { - if (me.getX() > mList.getWidth() - mThumbW - && me.getY() >= mThumbY - && me.getY() <= mThumbY + mThumbH) { - + + final int action = me.getAction(); + + if (action == MotionEvent.ACTION_DOWN) { + if (isPointInside(me.getX(), me.getY())) { setState(STATE_DRAGGING); if (mListAdapter == null && mList != null) { - getSections(); + getSectionsFromIndexer(); } cancelFling(); return true; } - } else if (me.getAction() == MotionEvent.ACTION_UP) { + } else if (action == MotionEvent.ACTION_UP) { if (mState == STATE_DRAGGING) { setState(STATE_VISIBLE); final Handler handler = mHandler; @@ -425,7 +435,7 @@ class FastScroller { handler.postDelayed(mScrollFade, 1000); return true; } - } else if (me.getAction() == MotionEvent.ACTION_MOVE) { + } else if (action == MotionEvent.ACTION_MOVE) { if (mState == STATE_DRAGGING) { final int viewHeight = mList.getHeight(); // Jitter @@ -448,7 +458,11 @@ class FastScroller { } return false; } - + + boolean isPointInside(float x, float y) { + return x > mList.getWidth() - mThumbW && y >= mThumbY && y <= mThumbY + mThumbH; + } + public class ScrollFade implements Runnable { long mStartTime; diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index 80fbf9e..3afd5d4 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -353,25 +353,24 @@ public class FrameLayout extends ViewGroup { if (mForeground != null) { final Drawable foreground = mForeground; + if (mForegroundBoundsChanged) { mForegroundBoundsChanged = false; - if (foreground != null) { - final Rect selfBounds = mSelfBounds; - final Rect overlayBounds = mOverlayBounds; - - final int w = mRight-mLeft; - final int h = mBottom-mTop; - - if (mForegroundInPadding) { - selfBounds.set(0, 0, w, h); - } else { - selfBounds.set(mPaddingLeft, mPaddingTop, w - mPaddingRight, h - mPaddingBottom); - } + final Rect selfBounds = mSelfBounds; + final Rect overlayBounds = mOverlayBounds; - Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), - foreground.getIntrinsicHeight(), selfBounds, overlayBounds); - foreground.setBounds(overlayBounds); + final int w = mRight-mLeft; + final int h = mBottom-mTop; + + if (mForegroundInPadding) { + selfBounds.set(0, 0, w, h); + } else { + selfBounds.set(mPaddingLeft, mPaddingTop, w - mPaddingRight, h - mPaddingBottom); } + + Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), + foreground.getIntrinsicHeight(), selfBounds, overlayBounds); + foreground.setBounds(overlayBounds); } foreground.draw(canvas); diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 02fc7c6..f86b37c 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -114,6 +114,8 @@ public class HorizontalScrollView extends FrameLayout { private boolean mSmoothScrollingEnabled = true; private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; public HorizontalScrollView(Context context) { this(context, null); @@ -179,7 +181,10 @@ public class HorizontalScrollView extends FrameLayout { setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setWillNotDraw(false); - mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + final ViewConfiguration configuration = ViewConfiguration.get(mContext); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); } @Override @@ -477,12 +482,10 @@ public class HorizontalScrollView extends FrameLayout { break; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; - velocityTracker.computeCurrentVelocity(1000); + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getXVelocity(); - if ((Math.abs(initialVelocity) > - ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity()) && - getChildCount() > 0) { + if ((Math.abs(initialVelocity) > mMinimumVelocity) && getChildCount() > 0) { fling(-initialVelocity); } diff --git a/core/java/android/widget/ImageButton.java b/core/java/android/widget/ImageButton.java index 4c1cbf6..d417e40 100644 --- a/core/java/android/widget/ImageButton.java +++ b/core/java/android/widget/ImageButton.java @@ -27,9 +27,35 @@ import java.util.Map; /** * <p> - * An image button displays an image that can be pressed, or clicked, by the - * user. - * </p> + * Displays a button with an image (instead of text) that can be pressed + * or clicked by the user. By default, an ImageButton looks like a regular + * {@link android.widget.Button}, with the standard button background + * that changes color during different button states. The image on the surface + * of the button is defined either by the {@code android:src} attribute in the + * {@code <ImageButton>} XML element or by the + * {@link #setImageResource(int)} method.</p> + * + * <p>To remove the standard button background image, define your own + * background image or set the background color to be transparent.</p> + * <p>To indicate the different button states (focused, selected, etc.), you can + * define a different image for each state. E.g., a blue image by default, an + * orange one for when focused, and a yellow one for when pressed. An easy way to + * do this is with an XML drawable "selector." For example:</p> + * <pre> + * <?xml version="1.0" encoding="utf-8"?> + * <selector xmlns:android="http://schemas.android.com/apk/res/android"> + * <item android:drawable="@drawable/button_normal" /> <!-- default --> + * <item android:state_pressed="true" + * android:drawable="@drawable/button_pressed" /> <!-- pressed --> + * <item android:state_focused="true" + * android:drawable="@drawable/button_focused" /> <!-- focused --> + * </selector></pre> + * + * <p>Save the XML file in your project {@code res/drawable/} folder and then + * reference it as a drawable for the source of your ImageButton (in the + * {@code android:src} attribute). Android will automatically change the image + * based on the state of the button and the corresponding images + * defined in the XML.</p> * * <p><strong>XML attributes</strong></p> * <p> diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 480b0b8..2796774 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -32,6 +32,8 @@ import android.net.Uri; import android.util.AttributeSet; import android.util.Log; import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.widget.RemoteViews.RemoteView; @@ -848,7 +850,7 @@ public class ImageView extends View { public int getBaseline() { return mBaselineAligned ? getMeasuredHeight() : -1; } - + /** * Set a tinting option for the image. * @@ -878,7 +880,7 @@ public class ImageView extends View { invalidate(); } } - + public void setAlpha(int alpha) { alpha &= 0xFF; // keep it legal if (mAlpha != alpha) { diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 5472d68..f8a6f89 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -21,6 +21,7 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.PixelFormat; +import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.graphics.drawable.ColorDrawable; import android.os.Parcel; @@ -35,6 +36,7 @@ import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewParent; import android.view.SoundEffectConstants; +import android.view.accessibility.AccessibilityEvent; import com.google.android.collect.Lists; import com.android.internal.R; @@ -132,6 +134,7 @@ public class ListView extends AbsListView { // used for temporary calculations. private final Rect mTempRect = new Rect(); + private Paint mDividerPaint; // the single allocated result per list view; kinda cheesey but avoids // allocating these thingies too often. @@ -171,6 +174,8 @@ public class ListView extends AbsListView { setDividerHeight(dividerHeight); } + setChoiceMode(a.getInt(R.styleable.ListView_choiceMode, CHOICE_MODE_NONE)); + mHeaderDividersEnabled = a.getBoolean(R.styleable.ListView_headerDividersEnabled, true); mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_footerDividersEnabled, true); @@ -1845,6 +1850,39 @@ public class ListView extends AbsListView { } } + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + boolean populated = super.dispatchPopulateAccessibilityEvent(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; + } + } + + event.setItemCount(itemCount); + event.setCurrentItemIndex(currentItemIndex); + } + + return populated; + } + /** * setSelectionAfterHeaderView set the selection to be the first list item * after the header views. @@ -2786,12 +2824,20 @@ public class ListView extends AbsListView { */ @Override public boolean isOpaque() { - return (mCachingStarted && mIsCacheColorOpaque && mDividerIsOpaque) || super.isOpaque(); + return (mCachingStarted && mIsCacheColorOpaque && mDividerIsOpaque && + hasOpaqueScrollbars()) || super.isOpaque(); } @Override public void setCacheColorHint(int color) { - mIsCacheColorOpaque = (color >>> 24) == 0xFF; + final boolean opaque = (color >>> 24) == 0xFF; + mIsCacheColorOpaque = opaque; + if (opaque) { + if (mDividerPaint == null) { + mDividerPaint = new Paint(); + } + mDividerPaint.setColor(color); + } super.setCacheColorHint(color); } @@ -2814,6 +2860,17 @@ public class ListView extends AbsListView { final int first = mFirstPosition; final boolean areAllItemsSelectable = mAreAllItemsSelectable; final ListAdapter adapter = mAdapter; + // If the list is opaque *and* the background is not, we want to + // fill a rect where the dividers would be for non-selectable items + // If the list is opaque and the background is also opaque, we don't + // need to draw anything since the background will do it for us + final boolean fillForMissingDividers = isOpaque() && !super.isOpaque(); + + if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) { + mDividerPaint = new Paint(); + mDividerPaint.setColor(getCacheColorHint()); + } + final Paint paint = mDividerPaint; if (!mStackFromBottom) { int bottom; @@ -2825,12 +2882,18 @@ public class ListView extends AbsListView { View child = getChildAt(i); bottom = child.getBottom(); // Don't draw dividers next to items that are not enabled - if (bottom < listBottom && (areAllItemsSelectable || - (adapter.isEnabled(first + i) && (i == count - 1 || - adapter.isEnabled(first + i + 1))))) { - bounds.top = bottom; - bounds.bottom = bottom + dividerHeight; - drawDivider(canvas, bounds, i); + if (bottom < listBottom) { + if ((areAllItemsSelectable || + (adapter.isEnabled(first + i) && (i == count - 1 || + adapter.isEnabled(first + i + 1))))) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + drawDivider(canvas, bounds, i); + } else if (fillForMissingDividers) { + bounds.top = bottom; + bounds.bottom = bottom + dividerHeight; + canvas.drawRect(bounds, paint); + } } } } @@ -2844,16 +2907,22 @@ public class ListView extends AbsListView { View child = getChildAt(i); top = child.getTop(); // Don't draw dividers next to items that are not enabled - if (top > listTop && (areAllItemsSelectable || - (adapter.isEnabled(first + i) && (i == count - 1 || - adapter.isEnabled(first + i + 1))))) { - bounds.top = top - dividerHeight; - bounds.bottom = top; - // Give the method the child ABOVE the divider, so we - // subtract one from our child - // position. Give -1 when there is no child above the - // divider. - drawDivider(canvas, bounds, i - 1); + if (top > listTop) { + if ((areAllItemsSelectable || + (adapter.isEnabled(first + i) && (i == count - 1 || + adapter.isEnabled(first + i + 1))))) { + bounds.top = top - dividerHeight; + bounds.bottom = top; + // Give the method the child ABOVE the divider, so we + // subtract one from our child + // position. Give -1 when there is no child above the + // divider. + drawDivider(canvas, bounds, i - 1); + } else if (fillForMissingDividers) { + bounds.top = top - dividerHeight; + bounds.bottom = top; + canvas.drawRect(bounds, paint); + } } } } @@ -3195,9 +3264,13 @@ public class ListView extends AbsListView { if (mChoiceMode == CHOICE_MODE_MULTIPLE) { mCheckStates.put(position, value); } else { - boolean oldValue = mCheckStates.get(position, false); + // Clear the old value: if something was selected and value == false + // then it is unselected mCheckStates.clear(); - if (!oldValue) { + // If value == true, select the appropriate position + // this may end up selecting the value we just cleared but this way + // we don't have to first to a get(position) + if (value) { mCheckStates.put(position, true); } } diff --git a/core/java/android/widget/MultiAutoCompleteTextView.java b/core/java/android/widget/MultiAutoCompleteTextView.java index 05abc26..ae80277 100644 --- a/core/java/android/widget/MultiAutoCompleteTextView.java +++ b/core/java/android/widget/MultiAutoCompleteTextView.java @@ -195,6 +195,8 @@ public class MultiAutoCompleteTextView extends AutoCompleteTextView { */ @Override protected void replaceText(CharSequence text) { + clearComposingText(); + int end = getSelectionEnd(); int start = mTokenizer.findTokenStart(getText(), end); diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index 78c7bd8..0c2cd55 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -18,6 +18,8 @@ package android.widget; import com.android.internal.R; +import android.content.Context; +import android.content.res.TypedArray; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; @@ -33,8 +35,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; import android.os.IBinder; -import android.content.Context; -import android.content.res.TypedArray; import android.util.AttributeSet; import java.lang.ref.WeakReference; @@ -49,7 +49,7 @@ import java.lang.ref.WeakReference; */ public class PopupWindow { /** - * Mode for {@link #setInputMethodMode(int): the requirements for the + * Mode for {@link #setInputMethodMode(int)}: the requirements for the * input method should be based on the focusability of the popup. That is * if it is focusable than it needs to work with the input method, else * it doesn't. @@ -57,16 +57,15 @@ public class PopupWindow { public static final int INPUT_METHOD_FROM_FOCUSABLE = 0; /** - * Mode for {@link #setInputMethodMode(int): this popup always needs to + * Mode for {@link #setInputMethodMode(int)}: this popup always needs to * work with an input method, regardless of whether it is focusable. This * means that it will always be displayed so that the user can also operate * the input method while it is shown. */ - public static final int INPUT_METHOD_NEEDED = 1; /** - * Mode for {@link #setInputMethodMode(int): this popup never needs to + * Mode for {@link #setInputMethodMode(int)}: this popup never needs to * work with an input method, regardless of whether it is focusable. This * means that it will always be displayed to use as much space on the * screen as needed, regardless of whether this covers the input method. @@ -823,6 +822,7 @@ public class PopupWindow { p.flags = computeFlags(p.flags); p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; p.token = token; + p.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; p.setTitle("PopupWindow:" + Integer.toHexString(hashCode())); return p; @@ -990,7 +990,7 @@ public class PopupWindow { int bottomEdge = displayFrame.bottom; if (ignoreBottomDecorations) { - bottomEdge = WindowManagerImpl.getDefault().getDefaultDisplay().getHeight(); + bottomEdge = anchor.getContext().getResources().getDisplayMetrics().heightPixels; } final int distanceToBottom = bottomEdge - (anchorPos[1] + anchor.getHeight()) - yOffset; final int distanceToTop = anchorPos[1] - displayFrame.top + yOffset; @@ -1017,6 +1017,7 @@ public class PopupWindow { unregisterForScrollChanged(); mWindowManager.removeView(mPopupView); + if (mPopupView != mContentView && mPopupView instanceof ViewGroup) { ((ViewGroup) mPopupView).removeView(mContentView); } @@ -1071,6 +1072,20 @@ public class PopupWindow { mWindowManager.updateViewLayout(mPopupView, p); } } + + /** + * <p>Updates the dimension of the popup window. Calling this function + * also updates the window with the current popup state as described + * for {@link #update()}.</p> + * + * @param width the new width + * @param height the new height + */ + public void update(int width, int height) { + WindowManager.LayoutParams p = (WindowManager.LayoutParams) + mPopupView.getLayoutParams(); + update(p.x, p.y, width, height, false); + } /** * <p>Updates the position and the dimension of the popup window. Width and @@ -1115,8 +1130,7 @@ public class PopupWindow { return; } - WindowManager.LayoutParams p = (WindowManager.LayoutParams) - mPopupView.getLayoutParams(); + WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); boolean update = force; @@ -1203,8 +1217,7 @@ public class PopupWindow { registerForScrollChanged(anchor, xoff, yoff); } - WindowManager.LayoutParams p = (WindowManager.LayoutParams) - mPopupView.getLayoutParams(); + WindowManager.LayoutParams p = (WindowManager.LayoutParams) mPopupView.getLayoutParams(); if (updateDimension) { if (width == -1) { @@ -1316,7 +1329,16 @@ public class PopupWindow { return super.onTouchEvent(event); } } - + + @Override + public void sendAccessibilityEvent(int eventType) { + // clinets are interested in the content not the container, make it event source + if (mContentView != null) { + mContentView.sendAccessibilityEvent(eventType); + } else { + super.sendAccessibilityEvent(eventType); + } + } } } diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index 441414a..2c9e71e 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -30,6 +30,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.StateListDrawable; +import android.graphics.drawable.Animatable; import android.graphics.drawable.shapes.RoundRectShape; import android.graphics.drawable.shapes.Shape; import android.util.AttributeSet; @@ -683,7 +684,7 @@ public class ProgressBar extends View { return; } - if (mIndeterminateDrawable instanceof AnimationDrawable) { + if (mIndeterminateDrawable instanceof Animatable) { mShouldStartAnimationDrawable = true; mAnimation = null; } else { @@ -708,8 +709,8 @@ public class ProgressBar extends View { void stopAnimation() { mAnimation = null; mTransformation = null; - if (mIndeterminateDrawable instanceof AnimationDrawable) { - ((AnimationDrawable) mIndeterminateDrawable).stop(); + if (mIndeterminateDrawable instanceof Animatable) { + ((Animatable) mIndeterminateDrawable).stop(); mShouldStartAnimationDrawable = false; } } @@ -818,8 +819,8 @@ public class ProgressBar extends View { } d.draw(canvas); canvas.restore(); - if (mShouldStartAnimationDrawable && d instanceof AnimationDrawable) { - ((AnimationDrawable) d).start(); + if (mShouldStartAnimationDrawable && d instanceof Animatable) { + ((Animatable) d).start(); mShouldStartAnimationDrawable = false; } } diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index edbb3db..e62dda5 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -16,40 +16,59 @@ package android.widget; +import com.android.internal.R; + import android.content.Context; import android.content.res.TypedArray; +import android.content.res.Resources; +import android.graphics.Rect; import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; +import android.util.SparseArray; +import android.util.Poolable; +import android.util.Pool; +import android.util.Pools; +import android.util.PoolableManager; +import static android.util.Log.d; import android.view.Gravity; +import android.view.View; import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import android.widget.RemoteViews.RemoteView; -import android.graphics.Rect; -import com.android.internal.R; +import java.util.Comparator; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.LinkedList; +import java.util.HashSet; +import java.util.ArrayList; /** * A Layout where the positions of the children can be described in relation to each other or to the * parent. For the sake of efficiency, the relations between views are evaluated in one pass, so if * view Y is dependent on the position of view X, make sure the view X comes first in the layout. - * + * * <p> * Note that you cannot have a circular dependency between the size of the RelativeLayout and the * position of its children. For example, you cannot have a RelativeLayout whose height is set to * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT WRAP_CONTENT} and a child set to * {@link #ALIGN_PARENT_BOTTOM}. * </p> - * + * * <p> * Also see {@link android.widget.RelativeLayout.LayoutParams RelativeLayout.LayoutParams} for * layout attributes * </p> - * + * * @attr ref android.R.styleable#RelativeLayout_gravity * @attr ref android.R.styleable#RelativeLayout_ignoreGravity */ @RemoteView public class RelativeLayout extends ViewGroup { + private static final String LOG_TAG = "RelativeLayout"; + + private static final boolean DEBUG_GRAPH = false; + public static final int TRUE = -1; /** @@ -137,6 +156,13 @@ public class RelativeLayout extends ViewGroup { private final Rect mSelfBounds = new Rect(); private int mIgnoreGravity; + private SortedSet<View> mTopToBottomLeftToRightSet = null; + + private boolean mDirtyHierarchy; + private View[] mSortedHorizontalChildren = new View[0]; + private View[] mSortedVerticalChildren = new View[0]; + private final DependencyGraph mGraph = new DependencyGraph(); + public RelativeLayout(Context context) { super(context); } @@ -225,7 +251,54 @@ public class RelativeLayout extends ViewGroup { } @Override + public void requestLayout() { + super.requestLayout(); + mDirtyHierarchy = true; + } + + private void sortChildren() { + int count = getChildCount(); + if (mSortedVerticalChildren.length != count) mSortedVerticalChildren = new View[count]; + if (mSortedHorizontalChildren.length != count) mSortedHorizontalChildren = new View[count]; + + final DependencyGraph graph = mGraph; + graph.clear(); + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + graph.add(child); + } + + if (DEBUG_GRAPH) { + d(LOG_TAG, "=== Sorted vertical children"); + graph.log(getResources(), ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM); + d(LOG_TAG, "=== Sorted horizontal children"); + graph.log(getResources(), LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT); + } + + graph.getSortedViews(mSortedVerticalChildren, ABOVE, BELOW, ALIGN_BASELINE, + ALIGN_TOP, ALIGN_BOTTOM); + graph.getSortedViews(mSortedHorizontalChildren, LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT); + + if (DEBUG_GRAPH) { + d(LOG_TAG, "=== Ordered list of vertical children"); + for (View view : mSortedVerticalChildren) { + DependencyGraph.printViewId(getResources(), view); + } + d(LOG_TAG, "=== Ordered list of horizontal children"); + for (View view : mSortedHorizontalChildren) { + DependencyGraph.printViewId(getResources(), view); + } + } + } + + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mDirtyHierarchy) { + mDirtyHierarchy = false; + sortChildren(); + } + int myWidth = -1; int myHeight = -1; @@ -254,7 +327,6 @@ public class RelativeLayout extends ViewGroup { height = myHeight; } - int len = this.getChildCount(); mHasBaselineAlignedChild = false; View ignore = null; @@ -268,22 +340,50 @@ public class RelativeLayout extends ViewGroup { int right = Integer.MIN_VALUE; int bottom = Integer.MIN_VALUE; + boolean offsetHorizontalAxis = false; + boolean offsetVerticalAxis = false; + if ((horizontalGravity || verticalGravity) && mIgnoreGravity != View.NO_ID) { ignore = findViewById(mIgnoreGravity); } - for (int i = 0; i < len; i++) { - View child = getChildAt(i); + final boolean isWrapContentWidth = widthMode != MeasureSpec.EXACTLY; + final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY; + + View[] views = mSortedHorizontalChildren; + int count = views.length; + for (int i = 0; i < count; i++) { + View child = views[i]; + if (child.getVisibility() != GONE) { + LayoutParams params = (LayoutParams) child.getLayoutParams(); + + applyHorizontalSizeRules(params, myWidth); + measureChildHorizontal(child, params, myWidth, myHeight); + if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) { + offsetHorizontalAxis = true; + } + } + } + + views = mSortedVerticalChildren; + count = views.length; + + for (int i = 0; i < count; i++) { + View child = views[i]; if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); - applySizeRules(params, myWidth, myHeight); + + applyVerticalSizeRules(params, myHeight); measureChild(child, params, myWidth, myHeight); - positionChild(child, params, myWidth, myHeight); + if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) { + offsetVerticalAxis = true; + } - if (widthMode != MeasureSpec.EXACTLY) { + if (isWrapContentWidth) { width = Math.max(width, params.mRight); } - if (heightMode != MeasureSpec.EXACTLY) { + + if (isWrapContentHeight) { height = Math.max(height, params.mBottom); } @@ -300,15 +400,15 @@ public class RelativeLayout extends ViewGroup { } if (mHasBaselineAlignedChild) { - for (int i = 0; i < len; i++) { + for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); alignBaseline(child, params); if (child != ignore || verticalGravity) { - left = Math.min(left, params.mLeft - params.leftMargin); - top = Math.min(top, params.mTop - params.topMargin); + left = Math.min(left, params.mLeft - params.leftMargin); + top = Math.min(top, params.mTop - params.topMargin); } if (child != ignore || horizontalGravity) { @@ -319,8 +419,8 @@ public class RelativeLayout extends ViewGroup { } } - if (widthMode != MeasureSpec.EXACTLY) { - // Width already has left padding in it since it was calculated by looking at + if (isWrapContentWidth) { + // Width already has left padding in it since it was calculated by looking at // the right of each child view width += mPaddingRight; @@ -330,9 +430,23 @@ public class RelativeLayout extends ViewGroup { width = Math.max(width, getSuggestedMinimumWidth()); width = resolveSize(width, widthMeasureSpec); + + if (offsetHorizontalAxis) { + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + LayoutParams params = (LayoutParams) child.getLayoutParams(); + final int[] rules = params.getRules(); + if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) { + centerHorizontal(child, params, width); + } + } + } + } } - if (heightMode != MeasureSpec.EXACTLY) { - // Height already has top padding in it since it was calculated by looking at + + if (isWrapContentHeight) { + // Height already has top padding in it since it was calculated by looking at // the bottom of each child view height += mPaddingBottom; @@ -342,6 +456,19 @@ public class RelativeLayout extends ViewGroup { height = Math.max(height, getSuggestedMinimumHeight()); height = resolveSize(height, heightMeasureSpec); + + if (offsetVerticalAxis) { + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + LayoutParams params = (LayoutParams) child.getLayoutParams(); + final int[] rules = params.getRules(); + if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) { + centerVertical(child, params, height); + } + } + } + } } if (horizontalGravity || verticalGravity) { @@ -355,7 +482,7 @@ public class RelativeLayout extends ViewGroup { final int horizontalOffset = contentBounds.left - left; final int verticalOffset = contentBounds.top - top; if (horizontalOffset != 0 || verticalOffset != 0) { - for (int i = 0; i < len; i++) { + for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE && child != ignore) { LayoutParams params = (LayoutParams) child.getLayoutParams(); @@ -409,9 +536,7 @@ public class RelativeLayout extends ViewGroup { * @param myWidth Width of the the RelativeLayout * @param myHeight Height of the RelativeLayout */ - private void measureChild(View child, LayoutParams params, int myWidth, - int myHeight) { - + private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) { int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, params.mRight, params.width, params.leftMargin, params.rightMargin, @@ -425,6 +550,21 @@ public class RelativeLayout extends ViewGroup { child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } + private void measureChildHorizontal(View child, LayoutParams params, int myWidth, int myHeight) { + int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, + params.mRight, params.width, + params.leftMargin, params.rightMargin, + mPaddingLeft, mPaddingRight, + myWidth); + int childHeightMeasureSpec; + if (params.width == LayoutParams.FILL_PARENT) { + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(myHeight, MeasureSpec.EXACTLY); + } else { + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(myHeight, MeasureSpec.AT_MOST); + } + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + /** * Get a measure spec that accounts for all of the constraints on this view. * This includes size contstraints imposed by the RelativeLayout as well as @@ -504,19 +644,9 @@ public class RelativeLayout extends ViewGroup { return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode); } - /** - * After the child has been measured, assign it a position. Some views may - * already have final values for l,t,r,b. Others may have one or both edges - * unfixed (i.e. set to -1) in each dimension. These will get positioned - * based on which edge is fixed, the view's desired dimension, and whether - * or not it is centered. - * - * @param child Child to position - * @param params LayoutParams associated with child - * @param myWidth Width of the the RelativeLayout - * @param myHeight Height of the RelativeLayout - */ - private void positionChild(View child, LayoutParams params, int myWidth, int myHeight) { + private boolean positionChildHorizontal(View child, LayoutParams params, int myWidth, + boolean wrapContent) { + int[] rules = params.getRules(); if (params.mLeft < 0 && params.mRight >= 0) { @@ -527,13 +657,26 @@ public class RelativeLayout extends ViewGroup { params.mRight = params.mLeft + child.getMeasuredWidth(); } else if (params.mLeft < 0 && params.mRight < 0) { // Both left and right vary - if (0 != rules[CENTER_IN_PARENT] || 0 != rules[CENTER_HORIZONTAL]) { - centerHorizontal(child, params, myWidth); + if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) { + if (!wrapContent) { + centerHorizontal(child, params, myWidth); + } else { + params.mLeft = mPaddingLeft + params.leftMargin; + params.mRight = params.mLeft + child.getMeasuredWidth(); + } + return true; } else { params.mLeft = mPaddingLeft + params.leftMargin; params.mRight = params.mLeft + child.getMeasuredWidth(); } } + return false; + } + + private boolean positionChildVertical(View child, LayoutParams params, int myHeight, + boolean wrapContent) { + + int[] rules = params.getRules(); if (params.mTop < 0 && params.mBottom >= 0) { // Bottom is fixed, but top varies @@ -543,26 +686,23 @@ public class RelativeLayout extends ViewGroup { params.mBottom = params.mTop + child.getMeasuredHeight(); } else if (params.mTop < 0 && params.mBottom < 0) { // Both top and bottom vary - if (0 != rules[CENTER_IN_PARENT] || 0 != rules[CENTER_VERTICAL]) { - centerVertical(child, params, myHeight); + if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) { + if (!wrapContent) { + centerVertical(child, params, myHeight); + } else { + params.mTop = mPaddingTop + params.topMargin; + params.mBottom = params.mTop + child.getMeasuredHeight(); + } + return true; } else { params.mTop = mPaddingTop + params.topMargin; params.mBottom = params.mTop + child.getMeasuredHeight(); } } + return false; } - /** - * Set l,t,r,b values in the LayoutParams for one view based on its layout rules. - * Big assumption #1: All antecedents of this view have been sized & positioned - * Big assumption #2: The dimensions of the parent view (the RelativeLayout) - * are already known if they are needed. - * - * @param childParams LayoutParams for the view being positioned - * @param myWidth Width of the the RelativeLayout - * @param myHeight Height of the RelativeLayout - */ - private void applySizeRules(LayoutParams childParams, int myWidth, int myHeight) { + private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth) { int[] rules = childParams.getRules(); RelativeLayout.LayoutParams anchorParams; @@ -622,6 +762,11 @@ public class RelativeLayout extends ViewGroup { // FIXME uh oh... } } + } + + private void applyVerticalSizeRules(LayoutParams childParams, int myHeight) { + int[] rules = childParams.getRules(); + RelativeLayout.LayoutParams anchorParams; childParams.mTop = -1; childParams.mBottom = -1; @@ -684,18 +829,16 @@ public class RelativeLayout extends ViewGroup { private View getRelatedView(int[] rules, int relation) { int id = rules[relation]; if (id != 0) { - View v = findViewById(id); - if (v == null) { - return null; - } + DependencyGraph.Node node = mGraph.mKeyNodes.get(id); + if (node == null) return null; + View v = node.view; // Find the first non-GONE view up the chain while (v.getVisibility() == View.GONE) { rules = ((LayoutParams) v.getLayoutParams()).getRules(); - v = v.findViewById(rules[relation]); - if (v == null) { - return null; - } + node = mGraph.mKeyNodes.get((rules[relation])); + if (node == null) return null; + v = node.view; } return v; @@ -782,6 +925,57 @@ public class RelativeLayout extends ViewGroup { return new LayoutParams(p); } + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + if (mTopToBottomLeftToRightSet == null) { + mTopToBottomLeftToRightSet = new TreeSet<View>(new TopToBottomLeftToRightComparator()); + } + + // sort children top-to-bottom and left-to-right + for (int i = 0, count = getChildCount(); i < count; i++) { + mTopToBottomLeftToRightSet.add(getChildAt(i)); + } + + for (View view : mTopToBottomLeftToRightSet) { + if (view.dispatchPopulateAccessibilityEvent(event)) { + mTopToBottomLeftToRightSet.clear(); + return true; + } + } + + mTopToBottomLeftToRightSet.clear(); + return false; + } + + /** + * Compares two views in left-to-right and top-to-bottom fashion. + */ + private class TopToBottomLeftToRightComparator implements Comparator<View> { + public int compare(View first, View second) { + // top - bottom + int topDifference = first.getTop() - second.getTop(); + if (topDifference != 0) { + return topDifference; + } + // left - right + int leftDifference = first.getLeft() - second.getLeft(); + if (leftDifference != 0) { + return leftDifference; + } + // break tie by height + int heightDiference = first.getHeight() - second.getHeight(); + if (heightDiference != 0) { + return heightDiference; + } + // break tie by width + int widthDiference = first.getWidth() - second.getWidth(); + if (widthDiference != 0) { + return widthDiference; + } + return 0; + } + } + /** * Per-child layout information associated with RelativeLayout. * @@ -823,7 +1017,7 @@ public class RelativeLayout extends ViewGroup { @ViewDebug.IntToString(from = RIGHT_OF, to = "rightOf") }, mapping = { @ViewDebug.IntToString(from = TRUE, to = "true"), - @ViewDebug.IntToString(from = 0, to = "FALSE/NO_ID") + @ViewDebug.IntToString(from = 0, to = "false/NO_ID") }) private int[] mRules = new int[VERB_COUNT]; @@ -975,4 +1169,284 @@ public class RelativeLayout extends ViewGroup { return mRules; } } + + private static class DependencyGraph { + /** + * List of all views in the graph. + */ + private ArrayList<Node> mNodes = new ArrayList<Node>(); + + /** + * List of nodes in the graph. Each node is identified by its + * view id (see View#getId()). + */ + private SparseArray<Node> mKeyNodes = new SparseArray<Node>(); + + /** + * Temporary data structure used to build the list of roots + * for this graph. + */ + private LinkedList<Node> mRoots = new LinkedList<Node>(); + + /** + * Clears the graph. + */ + void clear() { + final ArrayList<Node> nodes = mNodes; + final int count = nodes.size(); + + for (int i = 0; i < count; i++) { + nodes.get(i).release(); + } + nodes.clear(); + + mKeyNodes.clear(); + mRoots.clear(); + } + + /** + * Adds a view to the graph. + * + * @param view The view to be added as a node to the graph. + */ + void add(View view) { + final int id = view.getId(); + final Node node = Node.acquire(view); + + if (id != View.NO_ID) { + mKeyNodes.put(id, node); + } + + mNodes.add(node); + } + + /** + * Builds a sorted list of views. The sorting order depends on the dependencies + * between the view. For instance, if view C needs view A to be processed first + * and view A needs view B to be processed first, the dependency graph + * is: B -> A -> C. The sorted array will contain views B, A and C in this order. + * + * @param sorted The sorted list of views. The length of this array must + * be equal to getChildCount(). + * @param rules The list of rules to take into account. + */ + void getSortedViews(View[] sorted, int... rules) { + final LinkedList<Node> roots = findRoots(rules); + int index = 0; + + while (roots.size() > 0) { + final Node node = roots.removeFirst(); + final View view = node.view; + final int key = view.getId(); + + sorted[index++] = view; + + final HashSet<Node> dependents = node.dependents; + for (Node dependent : dependents) { + final SparseArray<Node> dependencies = dependent.dependencies; + + dependencies.remove(key); + if (dependencies.size() == 0) { + roots.add(dependent); + } + } + } + + if (index < sorted.length) { + throw new IllegalStateException("Circular dependencies cannot exist" + + " in RelativeLayout"); + } + } + + /** + * Finds the roots of the graph. A root is a node with no dependency and + * with [0..n] dependents. + * + * @param rulesFilter The list of rules to consider when building the + * dependencies + * + * @return A list of node, each being a root of the graph + */ + private LinkedList<Node> findRoots(int[] rulesFilter) { + final SparseArray<Node> keyNodes = mKeyNodes; + final ArrayList<Node> nodes = mNodes; + final int count = nodes.size(); + + // Find roots can be invoked several times, so make sure to clear + // all dependents and dependencies before running the algorithm + for (int i = 0; i < count; i++) { + final Node node = nodes.get(i); + node.dependents.clear(); + node.dependencies.clear(); + } + + // Builds up the dependents and dependencies for each node of the graph + for (int i = 0; i < count; i++) { + final Node node = nodes.get(i); + + final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams(); + final int[] rules = layoutParams.mRules; + final int rulesCount = rulesFilter.length; + + // Look only the the rules passed in parameter, this way we build only the + // dependencies for a specific set of rules + for (int j = 0; j < rulesCount; j++) { + final int rule = rules[rulesFilter[j]]; + if (rule > 0) { + // The node this node depends on + final Node dependency = keyNodes.get(rule); + if (dependency == node) { + throw new IllegalStateException("A view cannot have a dependency" + + " on itself"); + } + if (dependency == null) { + continue; + } + // Add the current node as a dependent + dependency.dependents.add(node); + // Add a dependency to the current node + node.dependencies.put(rule, dependency); + } + } + } + + final LinkedList<Node> roots = mRoots; + roots.clear(); + + // Finds all the roots in the graph: all nodes with no dependencies + for (int i = 0; i < count; i++) { + final Node node = nodes.get(i); + if (node.dependencies.size() == 0) roots.add(node); + } + + return roots; + } + + /** + * Prints the dependency graph for the specified rules. + * + * @param resources The context's resources to print the ids. + * @param rules The list of rules to take into account. + */ + void log(Resources resources, int... rules) { + final LinkedList<Node> roots = findRoots(rules); + for (Node node : roots) { + printNode(resources, node); + } + } + + static void printViewId(Resources resources, View view) { + if (view.getId() != View.NO_ID) { + d(LOG_TAG, resources.getResourceEntryName(view.getId())); + } else { + d(LOG_TAG, "NO_ID"); + } + } + + private static void appendViewId(Resources resources, Node node, StringBuilder buffer) { + if (node.view.getId() != View.NO_ID) { + buffer.append(resources.getResourceEntryName(node.view.getId())); + } else { + buffer.append("NO_ID"); + } + } + + private static void printNode(Resources resources, Node node) { + if (node.dependents.size() == 0) { + printViewId(resources, node.view); + } else { + for (Node dependent : node.dependents) { + StringBuilder buffer = new StringBuilder(); + appendViewId(resources, node, buffer); + printdependents(resources, dependent, buffer); + } + } + } + + private static void printdependents(Resources resources, Node node, StringBuilder buffer) { + buffer.append(" -> "); + appendViewId(resources, node, buffer); + + if (node.dependents.size() == 0) { + d(LOG_TAG, buffer.toString()); + } else { + for (Node dependent : node.dependents) { + StringBuilder subBuffer = new StringBuilder(buffer); + printdependents(resources, dependent, subBuffer); + } + } + } + + /** + * A node in the dependency graph. A node is a view, its list of dependencies + * and its list of dependents. + * + * A node with no dependent is considered a root of the graph. + */ + static class Node implements Poolable<Node> { + /** + * The view representing this node in the layout. + */ + View view; + + /** + * The list of dependents for this node; a dependent is a node + * that needs this node to be processed first. + */ + final HashSet<Node> dependents = new HashSet<Node>(); + + /** + * The list of dependencies for this node. + */ + final SparseArray<Node> dependencies = new SparseArray<Node>(); + + /* + * START POOL IMPLEMENTATION + */ + // The pool is static, so all nodes instances are shared across + // activities, that's why we give it a rather high limit + private static final int POOL_LIMIT = 100; + private static final Pool<Node> sPool = Pools.synchronizedPool( + Pools.finitePool(new PoolableManager<Node>() { + public Node newInstance() { + return new Node(); + } + + public void onAcquired(Node element) { + } + + public void onReleased(Node element) { + } + }, POOL_LIMIT) + ); + + private Node mNext; + + public void setNextPoolable(Node element) { + mNext = element; + } + + public Node getNextPoolable() { + return mNext; + } + + static Node acquire(View view) { + final Node node = sPool.acquire(); + node.view = view; + + return node; + } + + void release() { + view = null; + dependents.clear(); + dependencies.clear(); + + sPool.release(this); + } + /* + * END POOL IMPLEMENTATION + */ + } + } } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 7936f65..2dac652 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -20,10 +20,8 @@ import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.PorterDuff; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Parcel; @@ -36,15 +34,12 @@ import android.view.View; import android.view.ViewGroup; import android.view.LayoutInflater.Filter; import android.view.View.OnClickListener; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; import java.lang.Class; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; @@ -69,13 +64,7 @@ public class RemoteViews implements Parcelable, Filter { * The resource ID of the layout file. (Added to the parcel) */ private int mLayoutId; - - /** - * The Context object used to inflate the layout file. Also may - * be used by actions if they need access to the senders resources. - */ - private Context mContext; - + /** * An array of actions to perform on the view tree once it has been * inflated @@ -85,7 +74,7 @@ public class RemoteViews implements Parcelable, Filter { /** * This annotation indicates that a subclass of View is alllowed to be used - * with the {@link android.widget.RemoteViews} mechanism. + * with the {@link RemoteViews} mechanism. */ @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @@ -116,7 +105,7 @@ public class RemoteViews implements Parcelable, Filter { public int describeContents() { return 0; } - }; + } /** * Equivalent to calling @@ -232,15 +221,17 @@ public class RemoteViews implements Parcelable, Filter { targetDrawable = imageView.getDrawable(); } - // Perform modifications only if values are set correctly - if (alpha != -1) { - targetDrawable.setAlpha(alpha); - } - if (colorFilter != -1 && filterMode != null) { - targetDrawable.setColorFilter(colorFilter, filterMode); - } - if (level != -1) { - targetDrawable.setLevel(level); + if (targetDrawable != null) { + // Perform modifications only if values are set correctly + if (alpha != -1) { + targetDrawable.setAlpha(alpha); + } + if (colorFilter != -1 && filterMode != null) { + targetDrawable.setColorFilter(colorFilter, filterMode); + } + if (level != -1) { + targetDrawable.setLevel(level); + } } } @@ -289,6 +280,7 @@ public class RemoteViews implements Parcelable, Filter { this.viewId = in.readInt(); this.methodName = in.readString(); this.type = in.readInt(); + //noinspection ConstantIfStatement if (false) { Log.d("RemoteViews", "read viewId=0x" + Integer.toHexString(this.viewId) + " methodName=" + this.methodName + " type=" + this.type); @@ -340,31 +332,32 @@ public class RemoteViews implements Parcelable, Filter { out.writeInt(this.viewId); out.writeString(this.methodName); out.writeInt(this.type); + //noinspection ConstantIfStatement if (false) { Log.d("RemoteViews", "write viewId=0x" + Integer.toHexString(this.viewId) + " methodName=" + this.methodName + " type=" + this.type); } switch (this.type) { case BOOLEAN: - out.writeInt(((Boolean)this.value).booleanValue() ? 1 : 0); + out.writeInt((Boolean) this.value ? 1 : 0); break; case BYTE: - out.writeByte(((Byte)this.value).byteValue()); + out.writeByte((Byte) this.value); break; case SHORT: - out.writeInt(((Short)this.value).shortValue()); + out.writeInt((Short) this.value); break; case INT: - out.writeInt(((Integer)this.value).intValue()); + out.writeInt((Integer) this.value); break; case LONG: - out.writeLong(((Long)this.value).longValue()); + out.writeLong((Long) this.value); break; case FLOAT: - out.writeFloat(((Float)this.value).floatValue()); + out.writeFloat((Float) this.value); break; case DOUBLE: - out.writeDouble(((Double)this.value).doubleValue()); + out.writeDouble((Double) this.value); break; case CHAR: out.writeInt((int)((Character)this.value).charValue()); @@ -430,7 +423,7 @@ public class RemoteViews implements Parcelable, Filter { } Class klass = view.getClass(); - Method method = null; + Method method; try { method = klass.getMethod(this.methodName, getParameterType()); } @@ -446,6 +439,7 @@ public class RemoteViews implements Parcelable, Filter { } try { + //noinspection ConstantIfStatement if (false) { Log.d("RemoteViews", "view: " + klass.getName() + " calling method: " + this.methodName + "(" + param.getName() + ") with " @@ -816,13 +810,12 @@ public class RemoteViews implements Parcelable, Filter { * @return The inflated view hierarchy */ public View apply(Context context, ViewGroup parent) { - View result = null; + View result; Context c = prepareContext(context); - Resources r = c.getResources(); - LayoutInflater inflater = (LayoutInflater) c - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LayoutInflater inflater = (LayoutInflater) + c.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater = inflater.cloneInContext(c); inflater.setFilter(this); @@ -858,12 +851,12 @@ public class RemoteViews implements Parcelable, Filter { } private Context prepareContext(Context context) { - Context c = null; + Context c; String packageName = mPackage; if (packageName != null) { try { - c = context.createPackageContext(packageName, 0); + c = context.createPackageContext(packageName, Context.CONTEXT_RESTRICTED); } catch (NameNotFoundException e) { Log.e(LOG_TAG, "Package name " + packageName + " not found"); c = context; @@ -872,8 +865,6 @@ public class RemoteViews implements Parcelable, Filter { c = context; } - mContext = c; - return c; } diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index c9b3751..90e1242 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -115,6 +115,8 @@ public class ScrollView extends FrameLayout { private boolean mSmoothScrollingEnabled = true; private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; public ScrollView(Context context) { this(context, null); @@ -180,7 +182,10 @@ public class ScrollView extends FrameLayout { setFocusable(true); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setWillNotDraw(false); - mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + final ViewConfiguration configuration = ViewConfiguration.get(mContext); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); } @Override @@ -478,12 +483,10 @@ public class ScrollView extends FrameLayout { break; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; - velocityTracker.computeCurrentVelocity(1000); + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(); - if ((Math.abs(initialVelocity) > - ViewConfiguration.get(mContext).getScaledMinimumFlingVelocity()) && - getChildCount() > 0) { + if ((Math.abs(initialVelocity) > mMinimumVelocity) && getChildCount() > 0) { fling(-initialVelocity); } diff --git a/core/java/android/widget/SlidingDrawer.java b/core/java/android/widget/SlidingDrawer.java index 92561ed..f706744 100644 --- a/core/java/android/widget/SlidingDrawer.java +++ b/core/java/android/widget/SlidingDrawer.java @@ -16,21 +16,22 @@ package android.widget; -import android.view.ViewGroup; -import android.view.View; -import android.view.MotionEvent; -import android.view.VelocityTracker; -import android.view.SoundEffectConstants; +import android.R; import android.content.Context; import android.content.res.TypedArray; -import android.util.AttributeSet; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; -import android.graphics.Bitmap; -import android.os.SystemClock; import android.os.Handler; import android.os.Message; -import android.R; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; /** * SlidingDrawer hides content out of the screen and allows the user to drag a handle @@ -746,6 +747,8 @@ public class SlidingDrawer extends ViewGroup { openDrawer(); invalidate(); requestLayout(); + + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } /** @@ -777,6 +780,7 @@ public class SlidingDrawer extends ViewGroup { scrollListener.onScrollStarted(); } animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft()); + if (scrollListener != null) { scrollListener.onScrollEnded(); } @@ -798,6 +802,9 @@ public class SlidingDrawer extends ViewGroup { scrollListener.onScrollStarted(); } animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft()); + + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + if (scrollListener != null) { scrollListener.onScrollEnded(); } @@ -827,6 +834,7 @@ public class SlidingDrawer extends ViewGroup { } mExpanded = true; + if (mOnDrawerOpenListener != null) { mOnDrawerOpenListener.onDrawerOpened(); } diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java index dc2c70d..103d44d 100644 --- a/core/java/android/widget/TabHost.java +++ b/core/java/android/widget/TabHost.java @@ -87,8 +87,9 @@ public class TabHost extends FrameLayout implements ViewTreeObserver.OnTouchMode /** - * <p>Call setup() before adding tabs if loading TabHost using findViewById(). <i><b>However</i></b>: You do - * not need to call setup() after getTabHost() in {@link android.app.TabActivity TabActivity}. + * <p>Call setup() before adding tabs if loading TabHost using findViewById(). + * <i><b>However</i></b>: You do not need to call setup() after getTabHost() + * in {@link android.app.TabActivity TabActivity}. * Example:</p> <pre>mTabHost = (TabHost)findViewById(R.id.tabhost); mTabHost.setup(); @@ -176,7 +177,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); // leaving touch mode.. if nothing has focus, let's give it to // the indicator of the current tab if (!mCurrentView.hasFocus() || mCurrentView.isFocused()) { - mTabWidget.getChildAt(mCurrentTab).requestFocus(); + mTabWidget.getChildTabViewAt(mCurrentTab).requestFocus(); } } } @@ -196,6 +197,12 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); } View tabIndicator = tabSpec.mIndicatorStrategy.createIndicatorView(); tabIndicator.setOnKeyListener(mTabKeyListener); + + // If this is a custom view, then do not draw the bottom strips for + // the tab indicators. + if (tabSpec.mIndicatorStrategy instanceof ViewIndicatorStrategy) { + mTabWidget.setDrawBottomStrips(false); + } mTabWidget.addView(tabIndicator); mTabSpecs.add(tabSpec); @@ -234,7 +241,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); public View getCurrentTabView() { if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) { - return mTabWidget.getChildAt(mCurrentTab); + return mTabWidget.getChildTabViewAt(mCurrentTab); } return null; } @@ -272,7 +279,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); && (mCurrentView.isRootNamespace()) && (mCurrentView.hasFocus()) && (mCurrentView.findFocus().focusSearch(View.FOCUS_UP) == null)) { - mTabWidget.getChildAt(mCurrentTab).requestFocus(); + mTabWidget.getChildTabViewAt(mCurrentTab).requestFocus(); playSoundEffect(SoundEffectConstants.NAVIGATION_UP); return true; } @@ -363,14 +370,14 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); * * @param tag * Which tab was selected. - * @return The view to distplay the contents of the selected tab. + * @return The view to display the contents of the selected tab. */ View createTabContent(String tag); } /** - * A tab has a tab indictor, content, and a tag that is used to keep + * A tab has a tab indicator, content, and a tag that is used to keep * track of it. This builder helps choose among these options. * * For the tab indicator, your choices are: @@ -410,6 +417,14 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); } /** + * Specify a view as the tab indicator. + */ + public TabSpec setIndicator(View view) { + mIndicatorStrategy = new ViewIndicatorStrategy(view); + return this; + } + + /** * Specify the id of the view that should be used as the content * of the tab. */ @@ -436,7 +451,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); } - String getTag() { + public String getTag() { return mTag; } } @@ -525,6 +540,22 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); } /** + * How to create a tab indicator by specifying a view. + */ + private class ViewIndicatorStrategy implements IndicatorStrategy { + + private final View mView; + + private ViewIndicatorStrategy(View view) { + mView = view; + } + + public View createIndicatorView() { + return mView; + } + } + + /** * How to create the tab content via a view id. */ private class ViewIdContentStrategy implements ContentStrategy { @@ -607,7 +638,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); } mLaunchedView = wd; - // XXX Set FOCUS_AFTER_DESCENDANTS on embedded activies for now so they can get + // XXX Set FOCUS_AFTER_DESCENDANTS on embedded activities for now so they can get // focus if none of their children have it. They need focus to be able to // display menu items. // diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java index 20cddcb..a26bfa2 100644 --- a/core/java/android/widget/TabWidget.java +++ b/core/java/android/widget/TabWidget.java @@ -49,6 +49,8 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { private Drawable mBottomLeftStrip; private Drawable mBottomRightStrip; private boolean mStripMoved; + private Drawable mDividerDrawable; + private boolean mDrawBottomStrips = true; public TabWidget(Context context) { this(context, null); @@ -87,9 +89,68 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { setOnFocusChangeListener(this); } + /** + * Returns the tab indicator view at the given index. + * + * @param index the zero-based index of the tab indicator view to return + * @return the tab indicator view at the given index + */ + public View getChildTabViewAt(int index) { + // If we are using dividers, then instead of tab views at 0, 1, 2, ... + // we have tab views at 0, 2, 4, ... + if (mDividerDrawable != null) { + index *= 2; + } + return getChildAt(index); + } + + /** + * Returns the number of tab indicator views. + * @return the number of tab indicator views. + */ + public int getTabCount() { + int children = getChildCount(); + + // If we have dividers, then we will always have an odd number of + // children: 1, 3, 5, ... and we want to convert that sequence to + // this: 1, 2, 3, ... + if (mDividerDrawable != null) { + children = (children + 1) / 2; + } + return children; + } + + /** + * Sets the drawable to use as a divider between the tab indicators. + * @param drawable the divider drawable + */ + public void setDividerDrawable(Drawable drawable) { + mDividerDrawable = drawable; + } + + /** + * Sets the drawable to use as a divider between the tab indicators. + * @param resId the resource identifier of the drawable to use as a + * divider. + */ + public void setDividerDrawable(int resId) { + mDividerDrawable = mContext.getResources().getDrawable(resId); + } + + /** + * Controls whether the bottom strips on the tab indicators are drawn or + * not. The default is to draw them. If the user specifies a custom + * view for the tab indicators, then the TabHost class calls this method + * to disable drawing of the bottom strips. + * @param drawBottomStrips true if the bottom strips should be drawn. + */ + void setDrawBottomStrips(boolean drawBottomStrips) { + mDrawBottomStrips = drawBottomStrips; + } + @Override public void childDrawableStateChanged(View child) { - if (child == getChildAt(mSelectedTab)) { + if (child == getChildTabViewAt(mSelectedTab)) { // To make sure that the bottom strip is redrawn invalidate(); } @@ -100,7 +161,14 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { public void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); - View selectedChild = getChildAt(mSelectedTab); + // If the user specified a custom view for the tab indicators, then + // do not draw the bottom strips. + if (!mDrawBottomStrips) { + // Skip drawing the bottom strips. + return; + } + + View selectedChild = getChildTabViewAt(mSelectedTab); mBottomLeftStrip.setState(selectedChild.getDrawableState()); mBottomRightStrip.setState(selectedChild.getDrawableState()); @@ -157,13 +225,13 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { * @see #focusCurrentTab */ public void setCurrentTab(int index) { - if (index < 0 || index >= getChildCount()) { + if (index < 0 || index >= getTabCount()) { return; } - getChildAt(mSelectedTab).setSelected(false); + getChildTabViewAt(mSelectedTab).setSelected(false); mSelectedTab = index; - getChildAt(mSelectedTab).setSelected(true); + getChildTabViewAt(mSelectedTab).setSelected(true); mStripMoved = true; } @@ -189,17 +257,17 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { // change the focus if applicable. if (oldTab != index) { - getChildAt(index).requestFocus(); + getChildTabViewAt(index).requestFocus(); } } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); - int count = getChildCount(); + int count = getTabCount(); - for (int i=0; i<count; i++) { - View child = getChildAt(i); + for (int i = 0; i < count; i++) { + View child = getChildTabViewAt(i); child.setEnabled(enabled); } } @@ -218,17 +286,26 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { child.setFocusable(true); child.setClickable(true); + // If we have dividers between the tabs and we already have at least one + // tab, then add a divider before adding the next tab. + if (mDividerDrawable != null && getTabCount() > 0) { + View divider = new View(mContext); + final LinearLayout.LayoutParams lp = new LayoutParams( + mDividerDrawable.getIntrinsicWidth(), + mDividerDrawable.getIntrinsicHeight()); + lp.setMargins(0, 0, 0, 0); + divider.setLayoutParams(lp); + divider.setBackgroundDrawable(mDividerDrawable); + super.addView(divider); + } super.addView(child); // TODO: detect this via geometry with a tabwidget listener rather // than potentially interfere with the view's listener - child.setOnClickListener(new TabClickListener(getChildCount() - 1)); + child.setOnClickListener(new TabClickListener(getTabCount() - 1)); child.setOnFocusChangeListener(this); } - - - /** * Provides a way for {@link TabHost} to be notified that the user clicked on a tab indicator. */ @@ -238,14 +315,15 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { public void onFocusChange(View v, boolean hasFocus) { if (v == this && hasFocus) { - getChildAt(mSelectedTab).requestFocus(); + getChildTabViewAt(mSelectedTab).requestFocus(); return; } if (hasFocus) { int i = 0; - while (i < getChildCount()) { - if (getChildAt(i) == v) { + int numTabs = getTabCount(); + while (i < numTabs) { + if (getChildTabViewAt(i) == v) { setCurrentTab(i); mSelectionChangedListener.onTabSelectionChanged(i, false); break; diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index adfc74f..d8ed4f0 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -16,6 +16,11 @@ package android.widget; +import com.android.internal.util.FastMath; +import com.android.internal.widget.EditableInputConnection; + +import org.xmlpull.v1.XmlPullParserException; + import android.content.Context; import android.content.Intent; import android.content.res.ColorStateList; @@ -31,17 +36,17 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; +import android.os.Message; import android.os.Parcel; import android.os.Parcelable; import android.os.ResultReceiver; import android.os.SystemClock; -import android.os.Message; import android.text.BoringLayout; +import android.text.ClipboardManager; import android.text.DynamicLayout; import android.text.Editable; import android.text.GetChars; import android.text.GraphicsOperations; -import android.text.ClipboardManager; import android.text.InputFilter; import android.text.InputType; import android.text.Layout; @@ -49,9 +54,9 @@ import android.text.ParcelableSpan; import android.text.Selection; import android.text.SpanWatcher; import android.text.Spannable; +import android.text.SpannableString; import android.text.Spanned; import android.text.SpannedString; -import android.text.SpannableString; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; @@ -64,19 +69,18 @@ import android.text.method.KeyListener; import android.text.method.LinkMovementMethod; import android.text.method.MetaKeyKeyListener; import android.text.method.MovementMethod; -import android.text.method.TimeKeyListener; - import android.text.method.PasswordTransformationMethod; import android.text.method.SingleLineTransformationMethod; import android.text.method.TextKeyListener; +import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; import android.text.style.ParagraphStyle; import android.text.style.URLSpan; import android.text.style.UpdateAppearance; import android.text.util.Linkify; import android.util.AttributeSet; -import android.util.Log; import android.util.FloatMath; +import android.util.Log; import android.util.TypedValue; import android.view.ContextMenu; import android.view.Gravity; @@ -89,25 +93,22 @@ import android.view.ViewDebug; import android.view.ViewRoot; import android.view.ViewTreeObserver; import android.view.ViewGroup.LayoutParams; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.animation.AnimationUtils; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; +import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.EditorInfo; import android.widget.RemoteViews.RemoteView; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; -import com.android.internal.util.FastMath; -import com.android.internal.widget.EditableInputConnection; - -import org.xmlpull.v1.XmlPullParserException; - /** * Displays text to the user and optionally allows them to edit it. A TextView * is a complete text editor, however the basic class is configured to not @@ -126,6 +127,8 @@ import org.xmlpull.v1.XmlPullParserException; * @attr ref android.R.styleable#TextView_textColor * @attr ref android.R.styleable#TextView_textColorHighlight * @attr ref android.R.styleable#TextView_textColorHint + * @attr ref android.R.styleable#TextView_textAppearance + * @attr ref android.R.styleable#TextView_textColorLink * @attr ref android.R.styleable#TextView_textSize * @attr ref android.R.styleable#TextView_textScaleX * @attr ref android.R.styleable#TextView_typeface @@ -163,13 +166,22 @@ import org.xmlpull.v1.XmlPullParserException; * @attr ref android.R.styleable#TextView_capitalize * @attr ref android.R.styleable#TextView_autoText * @attr ref android.R.styleable#TextView_editable + * @attr ref android.R.styleable#TextView_freezesText + * @attr ref android.R.styleable#TextView_ellipsize * @attr ref android.R.styleable#TextView_drawableTop * @attr ref android.R.styleable#TextView_drawableBottom * @attr ref android.R.styleable#TextView_drawableRight * @attr ref android.R.styleable#TextView_drawableLeft + * @attr ref android.R.styleable#TextView_drawablePadding * @attr ref android.R.styleable#TextView_lineSpacingExtra * @attr ref android.R.styleable#TextView_lineSpacingMultiplier * @attr ref android.R.styleable#TextView_marqueeRepeatLimit + * @attr ref android.R.styleable#TextView_inputType + * @attr ref android.R.styleable#TextView_imeOptions + * @attr ref android.R.styleable#TextView_privateImeOptions + * @attr ref android.R.styleable#TextView_imeActionLabel + * @attr ref android.R.styleable#TextView_imeActionId + * @attr ref android.R.styleable#TextView_editorExtras */ @RemoteView public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { @@ -404,6 +416,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean singleLine = false; int maxlength = -1; CharSequence text = ""; + CharSequence hint = null; int shadowcolor = 0; float dx = 0, dy = 0, r = 0; boolean password = false; @@ -531,7 +544,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; case com.android.internal.R.styleable.TextView_hint: - setHint(a.getText(attr)); + hint = a.getText(attr); break; case com.android.internal.R.styleable.TextView_text: @@ -861,6 +874,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } setText(text, bufferType); + if (hint != null) setHint(hint); /* * Views are not normally focusable unless specified to be. @@ -1328,9 +1342,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else { // We need to retain the last set padding, so just clear // out all of the fields in the existing structure. + if (dr.mDrawableLeft != null) dr.mDrawableLeft.setCallback(null); dr.mDrawableLeft = null; + if (dr.mDrawableTop != null) dr.mDrawableTop.setCallback(null); dr.mDrawableTop = null; + if (dr.mDrawableRight != null) dr.mDrawableRight.setCallback(null); dr.mDrawableRight = null; + if (dr.mDrawableBottom != null) dr.mDrawableBottom.setCallback(null); dr.mDrawableBottom = null; dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0; dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0; @@ -1343,19 +1361,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mDrawables = dr = new Drawables(); } + if (dr.mDrawableLeft != left && dr.mDrawableLeft != null) { + dr.mDrawableLeft.setCallback(null); + } dr.mDrawableLeft = left; + if (dr.mDrawableTop != left && dr.mDrawableTop != null) { + dr.mDrawableTop.setCallback(null); + } dr.mDrawableTop = top; + if (dr.mDrawableRight != left && dr.mDrawableRight != null) { + dr.mDrawableRight.setCallback(null); + } dr.mDrawableRight = right; + if (dr.mDrawableBottom != left && dr.mDrawableBottom != null) { + dr.mDrawableBottom.setCallback(null); + } dr.mDrawableBottom = bottom; final Rect compoundRect = dr.mCompoundRect; - int[] state = null; + int[] state; state = getDrawableState(); if (left != null) { left.setState(state); left.copyBounds(compoundRect); + left.setCallback(this); dr.mDrawableSizeLeft = compoundRect.width(); dr.mDrawableHeightLeft = compoundRect.height(); } else { @@ -1365,6 +1396,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (right != null) { right.setState(state); right.copyBounds(compoundRect); + right.setCallback(this); dr.mDrawableSizeRight = compoundRect.width(); dr.mDrawableHeightRight = compoundRect.height(); } else { @@ -1374,6 +1406,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (top != null) { top.setState(state); top.copyBounds(compoundRect); + top.setCallback(this); dr.mDrawableSizeTop = compoundRect.height(); dr.mDrawableWidthTop = compoundRect.width(); } else { @@ -1383,6 +1416,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (bottom != null) { bottom.setState(state); bottom.copyBounds(compoundRect); + bottom.setCallback(this); dr.mDrawableSizeBottom = compoundRect.height(); dr.mDrawableWidthBottom = compoundRect.width(); } else { @@ -2785,8 +2819,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener checkForRelayout(); } - if (mText.length() == 0) + if (mText.length() == 0) { invalidate(); + } } /** @@ -3646,12 +3681,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override protected boolean isPaddingOffsetRequired() { - return mShadowRadius != 0; + return mShadowRadius != 0 || mDrawables != null; } @Override protected int getLeftPaddingOffset() { - return (int) Math.min(0, mShadowDx - mShadowRadius); + return getCompoundPaddingLeft() - mPaddingLeft + + (int) Math.min(0, mShadowDx - mShadowRadius); } @Override @@ -3666,7 +3702,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override protected int getRightPaddingOffset() { - return (int) Math.max(0, mShadowDx + mShadowRadius); + return -(getCompoundPaddingRight() - mPaddingRight) + + (int) Math.max(0, mShadowDx + mShadowRadius); } @Override @@ -3680,6 +3717,54 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override + public void invalidateDrawable(Drawable drawable) { + if (verifyDrawable(drawable)) { + final Rect dirty = drawable.getBounds(); + int scrollX = mScrollX; + int scrollY = mScrollY; + + // IMPORTANT: The coordinates below are based on the coordinates computed + // for each compound drawable in onDraw(). Make sure to update each section + // accordingly. + final TextView.Drawables drawables = mDrawables; + if (drawables != null) { + if (drawable == drawables.mDrawableLeft) { + final int compoundPaddingTop = getCompoundPaddingTop(); + final int compoundPaddingBottom = getCompoundPaddingBottom(); + final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; + + scrollX += mPaddingLeft; + scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2; + } else if (drawable == drawables.mDrawableRight) { + final int compoundPaddingTop = getCompoundPaddingTop(); + final int compoundPaddingBottom = getCompoundPaddingBottom(); + final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop; + + scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight); + scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2; + } else if (drawable == drawables.mDrawableTop) { + final int compoundPaddingLeft = getCompoundPaddingLeft(); + final int compoundPaddingRight = getCompoundPaddingRight(); + final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft; + + scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2; + scrollY += mPaddingTop; + } else if (drawable == drawables.mDrawableBottom) { + final int compoundPaddingLeft = getCompoundPaddingLeft(); + final int compoundPaddingRight = getCompoundPaddingRight(); + final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft; + + scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2; + scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom); + } + } + + invalidate(dirty.left + scrollX, dirty.top + scrollY, + dirty.right + scrollX, dirty.bottom + scrollY); + } + } + + @Override protected void onDraw(Canvas canvas) { restartMarqueeIfNeeded(); @@ -3707,6 +3792,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop; int hspace = right - left - compoundPaddingRight - compoundPaddingLeft; + // IMPORTANT: The coordinates computed are also used in invalidateDrawable() + // Make sure to update invalidateDrawable() when changing this code. if (dr.mDrawableLeft != null) { canvas.save(); canvas.translate(scrollX + mPaddingLeft, @@ -3716,6 +3803,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener canvas.restore(); } + // IMPORTANT: The coordinates computed are also used in invalidateDrawable() + // Make sure to update invalidateDrawable() when changing this code. if (dr.mDrawableRight != null) { canvas.save(); canvas.translate(scrollX + right - left - mPaddingRight - dr.mDrawableSizeRight, @@ -3724,6 +3813,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener canvas.restore(); } + // IMPORTANT: The coordinates computed are also used in invalidateDrawable() + // Make sure to update invalidateDrawable() when changing this code. if (dr.mDrawableTop != null) { canvas.save(); canvas.translate(scrollX + compoundPaddingLeft + (hspace - dr.mDrawableWidthTop) / 2, @@ -3732,6 +3823,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener canvas.restore(); } + // IMPORTANT: The coordinates computed are also used in invalidateDrawable() + // Make sure to update invalidateDrawable() when changing this code. if (dr.mDrawableBottom != null) { canvas.save(); canvas.translate(scrollX + compoundPaddingLeft + @@ -4714,10 +4807,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener alignment = Layout.Alignment.ALIGN_NORMAL; } + boolean shouldEllipsize = mEllipsize != null && mInput == null; + if (mText instanceof Spannable) { mLayout = new DynamicLayout(mText, mTransformed, mTextPaint, w, alignment, mSpacingMult, - mSpacingAdd, mIncludePad, mEllipsize, + mSpacingAdd, mIncludePad, mInput == null ? mEllipsize : null, ellipsisWidth); } else { if (boring == UNKNOWN_BORING) { @@ -4744,7 +4839,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Log.e("aaa", "Boring: " + mTransformed); mSavedLayout = (BoringLayout) mLayout; - } else if (mEllipsize != null && boring.width <= w) { + } else if (shouldEllipsize && boring.width <= w) { if (mSavedLayout != null) { mLayout = mSavedLayout. replaceOrMake(mTransformed, mTextPaint, @@ -4757,7 +4852,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boring, mIncludePad, mEllipsize, ellipsisWidth); } - } else if (mEllipsize != null) { + } else if (shouldEllipsize) { mLayout = new StaticLayout(mTransformed, 0, mTransformed.length(), mTextPaint, w, alignment, mSpacingMult, @@ -4769,7 +4864,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mIncludePad); // Log.e("aaa", "Boring but wide: " + mTransformed); } - } else if (mEllipsize != null) { + } else if (shouldEllipsize) { mLayout = new StaticLayout(mTransformed, 0, mTransformed.length(), mTextPaint, w, alignment, mSpacingMult, @@ -4782,9 +4877,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } + shouldEllipsize = mEllipsize != null; mHintLayout = null; if (mHint != null) { + if (shouldEllipsize) hintWidth = w; + if (hintBoring == UNKNOWN_BORING) { hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring); @@ -4794,24 +4892,50 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (hintBoring != null) { - if (hintBoring.width <= hintWidth) { + if (hintBoring.width <= hintWidth && + (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) { if (mSavedHintLayout != null) { mHintLayout = mSavedHintLayout. replaceOrMake(mHint, mTextPaint, - hintWidth, alignment, mSpacingMult, - mSpacingAdd, hintBoring, mIncludePad); + hintWidth, alignment, mSpacingMult, mSpacingAdd, + hintBoring, mIncludePad); } else { mHintLayout = BoringLayout.make(mHint, mTextPaint, - hintWidth, alignment, mSpacingMult, - mSpacingAdd, hintBoring, mIncludePad); + hintWidth, alignment, mSpacingMult, mSpacingAdd, + hintBoring, mIncludePad); } mSavedHintLayout = (BoringLayout) mHintLayout; + } else if (shouldEllipsize && hintBoring.width <= hintWidth) { + if (mSavedHintLayout != null) { + mHintLayout = mSavedHintLayout. + replaceOrMake(mHint, mTextPaint, + hintWidth, alignment, mSpacingMult, mSpacingAdd, + hintBoring, mIncludePad, mEllipsize, + ellipsisWidth); + } else { + mHintLayout = BoringLayout.make(mHint, mTextPaint, + hintWidth, alignment, mSpacingMult, mSpacingAdd, + hintBoring, mIncludePad, mEllipsize, + ellipsisWidth); + } + } else if (shouldEllipsize) { + mHintLayout = new StaticLayout(mHint, + 0, mHint.length(), + mTextPaint, hintWidth, alignment, mSpacingMult, + mSpacingAdd, mIncludePad, mEllipsize, + ellipsisWidth); } else { mHintLayout = new StaticLayout(mHint, mTextPaint, hintWidth, alignment, mSpacingMult, mSpacingAdd, mIncludePad); } + } else if (shouldEllipsize) { + mHintLayout = new StaticLayout(mHint, + 0, mHint.length(), + mTextPaint, hintWidth, alignment, mSpacingMult, + mSpacingAdd, mIncludePad, mEllipsize, + ellipsisWidth); } else { mHintLayout = new StaticLayout(mHint, mTextPaint, hintWidth, alignment, mSpacingMult, mSpacingAdd, @@ -4895,8 +5019,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private static final BoringLayout.Metrics UNKNOWN_BORING = - new BoringLayout.Metrics(); + private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics(); @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @@ -4923,8 +5046,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } if (des < 0) { - boring = BoringLayout.isBoring(mTransformed, mTextPaint, - mBoring); + boring = BoringLayout.isBoring(mTransformed, mTextPaint, mBoring); if (boring != null) { mBoring = boring; } @@ -4934,8 +5056,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (boring == null || boring == UNKNOWN_BORING) { if (des < 0) { - des = (int) FloatMath.ceil(Layout. - getDesiredWidth(mTransformed, mTextPaint)); + des = (int) FloatMath.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint)); } width = des; @@ -4953,13 +5074,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int hintDes = -1; int hintWidth; - if (mHintLayout != null) { + if (mHintLayout != null && mEllipsize == null) { hintDes = desired(mHintLayout); } if (hintDes < 0) { - hintBoring = BoringLayout.isBoring(mHint, mTextPaint, - mHintBoring); + hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mHintBoring); if (hintBoring != null) { mHintBoring = hintBoring; } @@ -4967,8 +5087,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (hintBoring == null || hintBoring == UNKNOWN_BORING) { if (hintDes < 0) { - hintDes = (int) FloatMath.ceil(Layout. - getDesiredWidth(mHint, mTextPaint)); + hintDes = (int) FloatMath.ceil( + Layout.getDesiredWidth(mHint, mTextPaint)); } hintWidth = hintDes; @@ -5014,20 +5134,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mLayout == null) { makeNewLayout(want, hintWant, boring, hintBoring, - width - getCompoundPaddingLeft() - getCompoundPaddingRight(), - false); + width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); } else if ((mLayout.getWidth() != want) || (hintWidth != hintWant) || (mLayout.getEllipsizedWidth() != width - getCompoundPaddingLeft() - getCompoundPaddingRight())) { if (mHint == null && mEllipsize == null && want > mLayout.getWidth() && (mLayout instanceof BoringLayout || - (fromexisting && des >= 0 && des <= want))) { + (fromexisting && des >= 0 && des <= want))) { mLayout.increaseWidthTo(want); } else { makeNewLayout(want, hintWant, boring, hintBoring, - width - getCompoundPaddingLeft() - getCompoundPaddingRight(), - false); + width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false); } } else { // Width has not changed. @@ -5048,11 +5166,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - int unpaddedHeight = height - getCompoundPaddingTop() - - getCompoundPaddingBottom(); + int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom(); if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) { - unpaddedHeight = Math.min(unpaddedHeight, - mLayout.getLineTop(mMaximum)); + unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum)); } /* @@ -5071,8 +5187,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private int getDesiredHeight() { - return Math.max(getDesiredHeight(mLayout, true), - getDesiredHeight(mHintLayout, false)); + return Math.max( + getDesiredHeight(mLayout, true), + getDesiredHeight(mHintLayout, mEllipsize != null)); } private int getDesiredHeight(Layout layout, boolean cap) { @@ -5715,6 +5832,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private void startMarquee() { + // Do not ellipsize EditText + if (mInput != null) return; + if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) { return; } @@ -6129,10 +6249,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private class ChangeWatcher implements TextWatcher, SpanWatcher { + + private CharSequence mBeforeText; + public void beforeTextChanged(CharSequence buffer, int start, int before, int after) { if (DEBUG_EXTRACT) Log.v(TAG, "beforeTextChanged start=" + start + " before=" + before + " after=" + after + ": " + buffer); + + if (AccessibilityManager.getInstance(mContext).isEnabled()) { + mBeforeText = buffer.toString(); + } + TextView.this.sendBeforeTextChanged(buffer, start, before, after); } @@ -6141,6 +6269,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (DEBUG_EXTRACT) Log.v(TAG, "onTextChanged start=" + start + " before=" + before + " after=" + after + ": " + buffer); TextView.this.handleTextChanged(buffer, start, before, after); + + if (AccessibilityManager.getInstance(mContext).isEnabled() && + (isFocused() || isSelected() && + isShown())) { + sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after); + mBeforeText = null; + } } public void afterTextChanged(Editable buffer) { @@ -6336,6 +6471,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener protected void onReceiveResult(int resultCode, Bundle resultData) { if (resultCode != InputMethodManager.RESULT_SHOWN) { + final int len = mText.length(); + if (mNewStart > len) { + mNewStart = len; + } + if (mNewEnd > len) { + mNewEnd = len; + } Selection.setSelection((Spannable)mText, mNewStart, mNewEnd); } } @@ -6525,9 +6667,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } else if (getLineCount() == 1) { switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: - return (mLayout.getLineRight(0) - mScrollX - (mRight - mLeft) - - getCompoundPaddingLeft() - getCompoundPaddingRight()) / - getHorizontalFadingEdgeLength(); + final int textWidth = (mRight - mLeft) - getCompoundPaddingLeft() - + getCompoundPaddingRight(); + final float lineWidth = mLayout.getLineWidth(0); + return (lineWidth - textWidth) / getHorizontalFadingEdgeLength(); case Gravity.RIGHT: return 0.0f; case Gravity.CENTER_HORIZONTAL: @@ -6776,6 +6919,40 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + boolean isPassword = + (mInputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION)) == + (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD); + + 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; + } + + void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, + int fromIndex, int removedCount, int addedCount) { + AccessibilityEvent event = + AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); + event.setFromIndex(fromIndex); + event.setRemovedCount(removedCount); + event.setAddedCount(addedCount); + event.setBeforeText(beforeText); + sendAccessibilityEventUnchecked(event); + } + + @Override protected void onCreateContextMenu(ContextMenu menu) { super.onCreateContextMenu(menu); boolean added = false; diff --git a/core/java/android/widget/Toast.java b/core/java/android/widget/Toast.java index ff74787..670692f 100644 --- a/core/java/android/widget/Toast.java +++ b/core/java/android/widget/Toast.java @@ -21,8 +21,8 @@ import android.app.ITransientNotification; import android.content.Context; import android.content.res.Resources; import android.graphics.PixelFormat; -import android.os.RemoteException; import android.os.Handler; +import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; import android.view.Gravity; @@ -278,7 +278,7 @@ public class Toast { } tv.setText(s); } - + // ======================================================================================= // All the gunk below is the interaction with the Notification Service, which handles // the proper ordering of these system-wide. @@ -373,6 +373,7 @@ public class Toast { TAG, "REMOVE! " + mView + " in " + this); mWM.removeView(mView); } + mView = null; } } diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index 6d3a2d3..20dd8a6 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -55,6 +55,7 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { private SurfaceHolder mSurfaceHolder = null; private MediaPlayer mMediaPlayer = null; private boolean mIsPrepared; + private boolean mIsPlaybackCompleted; private int mVideoWidth; private int mVideoHeight; private int mSurfaceWidth; @@ -260,7 +261,7 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { mSeekWhenPrepared = 0; } if (mStartWhenPrepared) { - mMediaPlayer.start(); + start(); mStartWhenPrepared = false; if (mMediaController != null) { mMediaController.show(); @@ -281,7 +282,7 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { mSeekWhenPrepared = 0; } if (mStartWhenPrepared) { - mMediaPlayer.start(); + start(); mStartWhenPrepared = false; } } @@ -291,6 +292,7 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { private MediaPlayer.OnCompletionListener mCompletionListener = new MediaPlayer.OnCompletionListener() { public void onCompletion(MediaPlayer mp) { + mIsPlaybackCompleted = true; if (mMediaController != null) { mMediaController.hide(); } @@ -405,7 +407,9 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { mMediaPlayer.seekTo(mSeekWhenPrepared); mSeekWhenPrepared = 0; } - mMediaPlayer.start(); + if (!mIsPlaybackCompleted) { + start(); + } if (mMediaController != null) { mMediaController.show(); } @@ -490,6 +494,7 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { } public void start() { + mIsPlaybackCompleted = false; if (mMediaPlayer != null && mIsPrepared) { mMediaPlayer.start(); mStartWhenPrepared = false; diff --git a/core/java/android/widget/ViewSwitcher.java b/core/java/android/widget/ViewSwitcher.java index f4f23a8..0dcaf95 100644 --- a/core/java/android/widget/ViewSwitcher.java +++ b/core/java/android/widget/ViewSwitcher.java @@ -16,8 +16,6 @@ package android.widget; -import java.util.Map; - import android.content.Context; import android.util.AttributeSet; import android.view.View; diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index d9fb78b..bae4dad 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -81,27 +81,27 @@ public class ZoomButtonsController implements View.OnTouchListener { private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20; private int mTouchPaddingScaledSq; - private Context mContext; - private WindowManager mWindowManager; + private final Context mContext; + private final WindowManager mWindowManager; private boolean mAutoDismissControls = true; /** * The view that is being zoomed by this zoom controller. */ - private View mOwnerView; + private final View mOwnerView; /** * The location of the owner view on the screen. This is recalculated * each time the zoom controller is shown. */ - private int[] mOwnerViewRawLocation = new int[2]; + private final int[] mOwnerViewRawLocation = new int[2]; /** * The container that is added as a window. */ - private FrameLayout mContainer; + private final FrameLayout mContainer; private LayoutParams mContainerLayoutParams; - private int[] mContainerRawLocation = new int[2]; + private final int[] mContainerRawLocation = new int[2]; private ZoomControls mControls; @@ -113,7 +113,7 @@ public class ZoomButtonsController implements View.OnTouchListener { /** * The {@link #mTouchTargetView}'s location in window, set on touch down. */ - private int[] mTouchTargetWindowLocation = new int[2]; + private final int[] mTouchTargetWindowLocation = new int[2]; /** * If the zoom controller is dismissed but the user is still in a touch @@ -128,8 +128,8 @@ public class ZoomButtonsController implements View.OnTouchListener { /** Whether the container has been added to the window manager. */ private boolean mIsVisible; - private Rect mTempRect = new Rect(); - private int[] mTempIntArray = new int[2]; + private final Rect mTempRect = new Rect(); + private final int[] mTempIntArray = new int[2]; private OnZoomListener mCallback; @@ -141,13 +141,13 @@ public class ZoomButtonsController implements View.OnTouchListener { */ private Runnable mPostedVisibleInitializer; - private IntentFilter mConfigurationChangedFilter = + private final IntentFilter mConfigurationChangedFilter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); /** * Needed to reposition the zoom controls after configuration changes. */ - private BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { + private final BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (!mIsVisible) return; @@ -167,7 +167,7 @@ public class ZoomButtonsController implements View.OnTouchListener { */ private static final int MSG_POST_SET_VISIBLE = 4; - private Handler mHandler = new Handler() { + private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { @@ -444,6 +444,9 @@ public class ZoomButtonsController implements View.OnTouchListener { } private void refreshPositioningVariables() { + // if the mOwnerView is detached from window then skip. + if (mOwnerView.getWindowToken() == null) return; + // Position the zoom controls on the bottom of the owner view. int ownerHeight = mOwnerView.getHeight(); int ownerWidth = mOwnerView.getWidth(); diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index e1ff2a5..4bac593 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -18,6 +18,8 @@ package com.android.internal.app; import com.android.internal.os.BatteryStatsImpl; +import android.telephony.SignalStrength; + interface IBatteryStats { byte[] getStatistics(); void noteStartWakelock(int uid, String name, int type); @@ -33,8 +35,9 @@ interface IBatteryStats { void noteUserActivity(int uid, int event); void notePhoneOn(); void notePhoneOff(); - void notePhoneSignalStrength(int asu); + void notePhoneSignalStrength(in SignalStrength signalStrength); void notePhoneDataConnectionState(int dataType, boolean hasData); + void noteAirplaneMode(boolean isAirplaneMode); void noteWifiOn(int uid); void noteWifiOff(int uid); void noteWifiRunning(); diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl index ce39768..af06965 100644 --- a/core/java/com/android/internal/backup/IBackupTransport.aidl +++ b/core/java/com/android/internal/backup/IBackupTransport.aidl @@ -16,6 +16,8 @@ package com.android.internal.backup; +import android.backup.RestoreSet; +import android.content.pm.PackageInfo; import android.os.ParcelFileDescriptor; /** {@hide} */ @@ -25,7 +27,7 @@ interface IBackupTransport { 1. set up the connection to the destination - set up encryption - for Google cloud, log in using the user's gaia credential or whatever - - for sd, spin off the backup transport and establish communication with it + - for adb, just set up the all-in-one destination file 2. send each app's backup transaction - parse the data file for key/value pointers etc - send key/blobsize set to the Google cloud, get back quota ok/rejected response @@ -36,34 +38,112 @@ interface IBackupTransport { - sd target streams raw data into encryption envelope then to sd? 3. shut down connection to destination - cloud: tear down connection etc - - sd: close the file and shut down the writer proxy + - adb: close the file */ /** - * Establish a connection to the back-end data repository, if necessary. If the transport - * needs to initialize state that is not tied to individual applications' backup operations, - * this is where it should be done. + * Ask the transport where, on local device storage, to keep backup state blobs. + * This is per-transport so that mock transports used for testing can coexist with + * "live" backup services without interfering with the live bookkeeping. The + * returned string should be a name that is expected to be unambiguous among all + * available backup transports; the name of the class implementing the transport + * is a good choice. * - * @return Zero on success; a nonzero error code on failure. + * @return A unique name, suitable for use as a file or directory name, that the + * Backup Manager could use to disambiguate state files associated with + * different backup transports. */ - int startSession(); + String transportDirName(); /** - * Send one application's data to the backup destination. + * Verify that this is a suitable time for a backup pass. This should return zero + * if a backup is reasonable right now, some positive value otherwise. This method + * will be called outside of the {@link #startSession}/{@link #endSession} pair. * - * @param packageName The identity of the application whose data is being backed up. + * <p>If this is not a suitable time for a backup, the transport should return a + * backoff delay, in milliseconds, after which the Backup Manager should try again. + * + * @return Zero if this is a suitable time for a backup pass, or a positive time delay + * in milliseconds to suggest deferring the backup pass for a while. + */ + long requestBackupTime(); + + /** + * Send one application's data to the backup destination. The transport may send + * the data immediately, or may buffer it. After this is called, {@link #finishBackup} + * must be called to ensure the data is sent and recorded successfully. + * + * @param packageInfo The identity of the application whose data is being backed up. + * This specifically includes the signature list for the package. * @param data The data stream that resulted from invoking the application's - * BackupService.doBackup() method. This may be a pipe rather than a - * file on persistent media, so it may not be seekable. - * @return Zero on success; a nonzero error code on failure. + * BackupService.doBackup() method. This may be a pipe rather than a file on + * persistent media, so it may not be seekable. + * @return false if errors occurred (the backup should be aborted and rescheduled), + * true if everything is OK so far (but {@link #finishBackup} must be called). + */ + boolean performBackup(in PackageInfo packageInfo, in ParcelFileDescriptor inFd); + + /** + * Erase the give application's data from the backup destination. This clears + * out the given package's data from the current backup set, making it as though + * the app had never yet been backed up. After this is called, {@link finishBackup} + * must be called to ensure that the operation is recorded successfully. + * + * @return false if errors occurred (the backup should be aborted and rescheduled), + * true if everything is OK so far (but {@link #finishBackup} must be called). + */ + boolean clearBackupData(in PackageInfo packageInfo); + + /** + * Finish sending application data to the backup destination. This must be + * called after {@link #performBackup} or {@link clearBackupData} to ensure that + * all data is sent. Only when this method returns true can a backup be assumed + * to have succeeded. + * + * @return false if errors occurred (the backup should be aborted and rescheduled), + * true if everything is OK. */ - int performBackup(String packageName, in ParcelFileDescriptor data); + boolean finishBackup(); /** - * Terminate the backup session, closing files, freeing memory, and cleaning up whatever - * other state the transport required. + * Get the set of backups currently available over this transport. * - * @return Zero on success; a nonzero error code on failure. Even on failure, the session - * is torn down and must be restarted if another backup is attempted. + * @return Descriptions of the set of restore images available for this device, + * or null if an error occurred (the attempt should be rescheduled). + **/ + RestoreSet[] getAvailableRestoreSets(); + + /** + * Start restoring application data from backup. After calling this function, + * alternate calls to {@link #nextRestorePackage} and {@link #nextRestoreData} + * to walk through the actual application data. + * + * @param token A backup token as returned by {@link #getAvailableRestoreSets}. + * @param packages List of applications to restore (if data is available). + * Application data will be restored in the order given. + * @return false if errors occurred (the restore should be aborted and rescheduled), + * true if everything is OK so far (go ahead and call {@link #nextRestorePackage}). + */ + boolean startRestore(long token, in PackageInfo[] packages); + + /** + * Get the package name of the next application with data in the backup store. + * @return The name of one of the packages supplied to {@link #startRestore}, + * or "" (the empty string) if no more backup data is available, + * or null if an error occurred (the restore should be aborted and rescheduled). + */ + String nextRestorePackage(); + + /** + * Get the data for the application returned by {@link #nextRestorePackage}. + * @param data An open, writable file into which the backup data should be stored. + * @return false if errors occurred (the restore should be aborted and rescheduled), + * true if everything is OK so far (go ahead and call {@link #nextRestorePackage}). + */ + boolean getRestoreData(in ParcelFileDescriptor outFd); + + /** + * End a restore session (aborting any in-process data transfer as necessary), + * freeing any resources and connections used during the restore process. */ - int endSession(); + void finishRestore(); } diff --git a/core/java/com/android/internal/backup/LocalTransport.java b/core/java/com/android/internal/backup/LocalTransport.java new file mode 100644 index 0000000..2facce2 --- /dev/null +++ b/core/java/com/android/internal/backup/LocalTransport.java @@ -0,0 +1,200 @@ +package com.android.internal.backup; + +import android.backup.BackupDataInput; +import android.backup.BackupDataOutput; +import android.backup.RestoreSet; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.Log; + +import org.bouncycastle.util.encoders.Base64; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; + +/** + * Backup transport for stashing stuff into a known location on disk, and + * later restoring from there. For testing only. + */ + +public class LocalTransport extends IBackupTransport.Stub { + private static final String TAG = "LocalTransport"; + private static final boolean DEBUG = true; + + private static final String TRANSPORT_DIR_NAME + = "com.android.internal.backup.LocalTransport"; + + private Context mContext; + private PackageManager mPackageManager; + private File mDataDir = new File(Environment.getDownloadCacheDirectory(), "backup"); + private PackageInfo[] mRestorePackages = null; + private int mRestorePackage = -1; // Index into mRestorePackages + + + public LocalTransport(Context context) { + if (DEBUG) Log.v(TAG, "Transport constructed"); + mContext = context; + mPackageManager = context.getPackageManager(); + } + + + public String transportDirName() throws RemoteException { + return TRANSPORT_DIR_NAME; + } + + public long requestBackupTime() throws RemoteException { + // any time is a good time for local backup + return 0; + } + + public boolean performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) + throws RemoteException { + if (DEBUG) Log.v(TAG, "performBackup() pkg=" + packageInfo.packageName); + + File packageDir = new File(mDataDir, packageInfo.packageName); + packageDir.mkdirs(); + + // Each 'record' in the restore set is kept in its own file, named by + // the record key. Wind through the data file, extracting individual + // record operations and building a set of all the updates to apply + // in this update. + BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor()); + try { + int bufSize = 512; + byte[] buf = new byte[bufSize]; + while (changeSet.readNextHeader()) { + String key = changeSet.getKey(); + String base64Key = new String(Base64.encode(key.getBytes())); + File entityFile = new File(packageDir, base64Key); + + int dataSize = changeSet.getDataSize(); + + if (DEBUG) Log.v(TAG, "Got change set key=" + key + " size=" + dataSize + + " key64=" + base64Key); + + if (dataSize >= 0) { + FileOutputStream entity = new FileOutputStream(entityFile); + + if (dataSize > bufSize) { + bufSize = dataSize; + buf = new byte[bufSize]; + } + changeSet.readEntityData(buf, 0, dataSize); + if (DEBUG) Log.v(TAG, " data size " + dataSize); + + try { + entity.write(buf, 0, dataSize); + } catch (IOException e) { + Log.e(TAG, "Unable to update key file " + entityFile.getAbsolutePath()); + return false; + } finally { + entity.close(); + } + } else { + entityFile.delete(); + } + } + return true; + } catch (IOException e) { + // oops, something went wrong. abort the operation and return error. + Log.v(TAG, "Exception reading backup input:", e); + return false; + } + } + + public boolean clearBackupData(PackageInfo packageInfo) { + if (DEBUG) Log.v(TAG, "clearBackupData() pkg=" + packageInfo.packageName); + + File packageDir = new File(mDataDir, packageInfo.packageName); + for (File f : packageDir.listFiles()) { + f.delete(); + } + packageDir.delete(); + return true; + } + + public boolean finishBackup() throws RemoteException { + if (DEBUG) Log.v(TAG, "finishBackup()"); + return true; + } + + // Restore handling + public RestoreSet[] getAvailableRestoreSets() throws android.os.RemoteException { + // one hardcoded restore set + RestoreSet set = new RestoreSet("Local disk image", "flash", 0); + RestoreSet[] array = { set }; + return array; + } + + public boolean startRestore(long token, PackageInfo[] packages) { + if (DEBUG) Log.v(TAG, "start restore " + token); + mRestorePackages = packages; + mRestorePackage = -1; + return true; + } + + public String nextRestorePackage() { + if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); + while (++mRestorePackage < mRestorePackages.length) { + String name = mRestorePackages[mRestorePackage].packageName; + if (new File(mDataDir, name).isDirectory()) { + if (DEBUG) Log.v(TAG, " nextRestorePackage() = " + name); + return name; + } + } + + if (DEBUG) Log.v(TAG, " no more packages to restore"); + return ""; + } + + public boolean getRestoreData(ParcelFileDescriptor outFd) { + if (mRestorePackages == null) throw new IllegalStateException("startRestore not called"); + if (mRestorePackage < 0) throw new IllegalStateException("nextRestorePackage not called"); + File packageDir = new File(mDataDir, mRestorePackages[mRestorePackage].packageName); + + // The restore set is the concatenation of the individual record blobs, + // each of which is a file in the package's directory + File[] blobs = packageDir.listFiles(); + if (blobs == null) { + Log.e(TAG, "Error listing directory: " + packageDir); + return false; // nextRestorePackage() ensures the dir exists, so this is an error + } + + // We expect at least some data if the directory exists in the first place + if (DEBUG) Log.v(TAG, " getRestoreData() found " + blobs.length + " key files"); + BackupDataOutput out = new BackupDataOutput(outFd.getFileDescriptor()); + try { + for (File f : blobs) { + FileInputStream in = new FileInputStream(f); + try { + int size = (int) f.length(); + byte[] buf = new byte[size]; + in.read(buf); + String key = new String(Base64.decode(f.getName())); + if (DEBUG) Log.v(TAG, " ... key=" + key + " size=" + size); + out.writeEntityHeader(key, size); + out.writeEntityData(buf, size); + } finally { + in.close(); + } + } + return true; + } catch (IOException e) { + Log.e(TAG, "Unable to read backup records", e); + return false; + } + } + + public void finishRestore() { + if (DEBUG) Log.v(TAG, "finishRestore()"); + } +} diff --git a/core/java/com/android/internal/backup/SystemBackupAgent.java b/core/java/com/android/internal/backup/SystemBackupAgent.java new file mode 100644 index 0000000..6b396d7 --- /dev/null +++ b/core/java/com/android/internal/backup/SystemBackupAgent.java @@ -0,0 +1,35 @@ +/* + * 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 com.android.internal.backup; + +import android.backup.AbsoluteFileBackupHelper; +import android.backup.BackupHelperAgent; + +/** + * Backup agent for various system-managed data + */ +public class SystemBackupAgent extends BackupHelperAgent { + // the set of files that we back up whole, as absolute paths + String[] mFiles = { + /* WallpaperService.WALLPAPER_FILE */ + "/data/data/com.android.settings/files/wallpaper", + }; + + public void onCreate() { + addHelper("system_files", new AbsoluteFileBackupHelper(this, mFiles)); + } +} diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index e8356a2..a03802d 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -23,23 +23,24 @@ import android.os.ParcelFormatException; import android.os.Parcelable; import android.os.Process; import android.os.SystemClock; +import android.telephony.SignalStrength; import android.telephony.TelephonyManager; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.SparseArray; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.FileReader; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.Map; -import java.util.Set; /** * All information we are collecting about things that can happen that impact @@ -54,7 +55,7 @@ public final class BatteryStatsImpl extends BatteryStats { private static final int MAGIC = 0xBA757475; // 'BATSTATS' // Current on-disk Parcel version - private static final int VERSION = 34; + private static final int VERSION = 39; private final File mFile; private final File mBackupFile; @@ -95,7 +96,7 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mScreenOn; StopwatchTimer mScreenOnTimer; - + int mScreenBrightnessBin = -1; final StopwatchTimer[] mScreenBrightnessTimer = new StopwatchTimer[NUM_SCREEN_BRIGHTNESS_BINS]; @@ -104,6 +105,12 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mPhoneOn; StopwatchTimer mPhoneOnTimer; + boolean mAudioOn; + StopwatchTimer mAudioOnTimer; + + boolean mVideoOn; + StopwatchTimer mVideoOnTimer; + int mPhoneSignalStrengthBin = -1; final StopwatchTimer[] mPhoneSignalStrengthsTimer = new StopwatchTimer[NUM_SIGNAL_STRENGTH_BINS]; @@ -132,18 +139,27 @@ public final class BatteryStatsImpl extends BatteryStats { long mTrackBatteryUptimeStart; long mTrackBatteryPastRealtime; long mTrackBatteryRealtimeStart; - + long mUnpluggedBatteryUptime; long mUnpluggedBatteryRealtime; - + /* * These keep track of battery levels (1-100) at the last plug event and the last unplug event. */ int mDischargeStartLevel; int mDischargeCurrentLevel; - + long mLastWriteTime = 0; // Milliseconds - + + // Mobile data transferred while on battery + private long[] mMobileDataTx = new long[4]; + private long[] mMobileDataRx = new long[4]; + private long[] mTotalDataTx = new long[4]; + private long[] mTotalDataRx = new long[4]; + + private long mRadioDataUptime; + private long mRadioDataStart; + /* * Holds a SamplingTimer associated with each kernel wakelock name being tracked. */ @@ -175,6 +191,8 @@ public final class BatteryStatsImpl extends BatteryStats { private final Map<String, KernelWakelockStats> mProcWakelockFileStats = new HashMap<String, KernelWakelockStats>(); + private HashMap<String, Integer> mUidCache = new HashMap<String, Integer>(); + // For debugging public BatteryStatsImpl() { mFile = mBackupFile = null; @@ -319,6 +337,13 @@ public final class BatteryStatsImpl extends BatteryStats { */ long mUnpluggedTime; + /** + * Constructs from a parcel. + * @param type + * @param unpluggables + * @param powerType + * @param in + */ Timer(int type, ArrayList<Unpluggable> unpluggables, Parcel in) { mType = type; @@ -631,7 +656,6 @@ public final class BatteryStatsImpl extends BatteryStats { * was actually held for an interesting duration. */ long mAcquireTime; - StopwatchTimer(int type, ArrayList<StopwatchTimer> timerPool, ArrayList<Unpluggable> unpluggables, Parcel in) { @@ -692,6 +716,10 @@ public final class BatteryStatsImpl extends BatteryStats { } } + boolean isRunningLocked() { + return mNesting > 0; + } + void stopRunningLocked(BatteryStatsImpl stats) { // Ignore attempt to stop a timer that isn't running if (mNesting == 0) { @@ -882,7 +910,40 @@ public final class BatteryStatsImpl extends BatteryStats { } return kwlt; } - + + private void doDataPlug(long[] dataTransfer, long currentBytes) { + dataTransfer[STATS_LAST] = dataTransfer[STATS_UNPLUGGED]; + dataTransfer[STATS_UNPLUGGED] = -1; + } + + private void doDataUnplug(long[] dataTransfer, long currentBytes) { + dataTransfer[STATS_UNPLUGGED] = currentBytes; + } + + private long getCurrentRadioDataUptimeMs() { + try { + File awakeTimeFile = new File("/sys/devices/virtual/net/rmnet0/awake_time_ms"); + if (!awakeTimeFile.exists()) return 0; + BufferedReader br = new BufferedReader(new FileReader(awakeTimeFile)); + String line = br.readLine(); + br.close(); + return Long.parseLong(line); + } catch (NumberFormatException nfe) { + // Nothing + } catch (IOException ioe) { + // Nothing + } + return 0; + } + + public long getRadioDataUptimeMs() { + if (mRadioDataStart == -1) { + return mRadioDataUptime; + } else { + return getCurrentRadioDataUptimeMs() - mRadioDataStart; + } + } + public void doUnplug(long batteryUptime, long batteryRealtime) { for (int iu = mUidStats.size() - 1; iu >= 0; iu--) { Uid u = mUidStats.valueAt(iu); @@ -894,8 +955,16 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i = mUnpluggables.size() - 1; i >= 0; i--) { mUnpluggables.get(i).unplug(batteryUptime, batteryRealtime); } + // Track total mobile data + doDataUnplug(mMobileDataRx, NetStat.getMobileRxBytes()); + doDataUnplug(mMobileDataTx, NetStat.getMobileTxBytes()); + doDataUnplug(mTotalDataRx, NetStat.getTotalRxBytes()); + doDataUnplug(mTotalDataTx, NetStat.getTotalTxBytes()); + // Track radio awake time + mRadioDataStart = getCurrentRadioDataUptimeMs(); + mRadioDataUptime = 0; } - + public void doPlug(long batteryUptime, long batteryRealtime) { for (int iu = mUidStats.size() - 1; iu >= 0; iu--) { Uid u = mUidStats.valueAt(iu); @@ -911,16 +980,23 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i = mUnpluggables.size() - 1; i >= 0; i--) { mUnpluggables.get(i).plug(batteryUptime, batteryRealtime); } + doDataPlug(mMobileDataRx, NetStat.getMobileRxBytes()); + doDataPlug(mMobileDataTx, NetStat.getMobileTxBytes()); + doDataPlug(mTotalDataRx, NetStat.getTotalRxBytes()); + doDataPlug(mTotalDataTx, NetStat.getTotalTxBytes()); + // Track radio awake time + mRadioDataUptime = getRadioDataUptimeMs(); + mRadioDataStart = -1; } - + public void noteStartGps(int uid) { - mUidStats.get(uid).noteStartGps(); + getUidStatsLocked(uid).noteStartGps(); } public void noteStopGps(int uid) { - mUidStats.get(uid).noteStopGps(); + getUidStatsLocked(uid).noteStopGps(); } - + public void noteScreenOnLocked() { if (!mScreenOn) { mScreenOn = true; @@ -962,10 +1038,7 @@ public final class BatteryStatsImpl extends BatteryStats { } public void noteUserActivityLocked(int uid, int event) { - Uid u = mUidStats.get(uid); - if (u != null) { - u.noteUserActivityLocked(event); - } + getUidStatsLocked(uid).noteUserActivityLocked(event); } public void notePhoneOnLocked() { @@ -981,15 +1054,43 @@ public final class BatteryStatsImpl extends BatteryStats { mPhoneOnTimer.stopRunningLocked(this); } } - - public void notePhoneSignalStrengthLocked(int asu) { + + public void noteAirplaneModeLocked(boolean isAirplaneMode) { + final int bin = mPhoneSignalStrengthBin; + if (bin >= 0) { + if (!isAirplaneMode) { + if (!mPhoneSignalStrengthsTimer[bin].isRunningLocked()) { + mPhoneSignalStrengthsTimer[bin].startRunningLocked(this); + } + } else { + for (int i = 0; i < NUM_SIGNAL_STRENGTH_BINS; i++) { + while (mPhoneSignalStrengthsTimer[i].isRunningLocked()) { + mPhoneSignalStrengthsTimer[i].stopRunningLocked(this); + } + } + } + } + } + + public void notePhoneSignalStrengthLocked(SignalStrength signalStrength) { // Bin the strength. int bin; - if (asu < 0 || asu >= 99) bin = SIGNAL_STRENGTH_NONE_OR_UNKNOWN; - else if (asu >= 16) bin = SIGNAL_STRENGTH_GREAT; - else if (asu >= 8) bin = SIGNAL_STRENGTH_GOOD; - else if (asu >= 4) bin = SIGNAL_STRENGTH_MODERATE; - else bin = SIGNAL_STRENGTH_POOR; + + if (!signalStrength.isGsm()) { + int dBm = signalStrength.getCdmaDbm(); + if (dBm >= -75) bin = SIGNAL_STRENGTH_NONE_OR_UNKNOWN; + else if (dBm >= -85) bin = SIGNAL_STRENGTH_GREAT; + else if (dBm >= -95) bin = SIGNAL_STRENGTH_GOOD; + else if (dBm >= -100) bin = SIGNAL_STRENGTH_MODERATE; + else bin = SIGNAL_STRENGTH_POOR; + } else { + int asu = signalStrength.getGsmSignalStrength(); + if (asu < 0 || asu >= 99) bin = SIGNAL_STRENGTH_NONE_OR_UNKNOWN; + else if (asu >= 16) bin = SIGNAL_STRENGTH_GREAT; + else if (asu >= 8) bin = SIGNAL_STRENGTH_GOOD; + else if (asu >= 4) bin = SIGNAL_STRENGTH_MODERATE; + else bin = SIGNAL_STRENGTH_POOR; + } if (mPhoneSignalStrengthBin != bin) { if (mPhoneSignalStrengthBin >= 0) { mPhoneSignalStrengthsTimer[mPhoneSignalStrengthBin].stopRunningLocked(this); @@ -1017,6 +1118,7 @@ public final class BatteryStatsImpl extends BatteryStats { break; } } + if (DEBUG) Log.i(TAG, "Phone Data Connection -> " + dataType + " = " + hasData); if (mPhoneDataConnectionType != bin) { if (mPhoneDataConnectionType >= 0) { mPhoneDataConnectionsTimer[mPhoneDataConnectionType].stopRunningLocked(this); @@ -1033,16 +1135,10 @@ public final class BatteryStatsImpl extends BatteryStats { } if (mWifiOnUid != uid) { if (mWifiOnUid >= 0) { - Uid u = mUidStats.get(mWifiOnUid); - if (u != null) { - u.noteWifiTurnedOffLocked(); - } + getUidStatsLocked(mWifiOnUid).noteWifiTurnedOffLocked(); } mWifiOnUid = uid; - Uid u = mUidStats.get(uid); - if (u != null) { - u.noteWifiTurnedOnLocked(); - } + getUidStatsLocked(uid).noteWifiTurnedOnLocked(); } } @@ -1052,14 +1148,43 @@ public final class BatteryStatsImpl extends BatteryStats { mWifiOnTimer.stopRunningLocked(this); } if (mWifiOnUid >= 0) { - Uid u = mUidStats.get(mWifiOnUid); - if (u != null) { - u.noteWifiTurnedOffLocked(); - } + getUidStatsLocked(mWifiOnUid).noteWifiTurnedOffLocked(); mWifiOnUid = -1; } } + + public void noteAudioOnLocked(int uid) { + if (!mAudioOn) { + mAudioOn = true; + mAudioOnTimer.startRunningLocked(this); + } + getUidStatsLocked(uid).noteAudioTurnedOnLocked(); + } + public void noteAudioOffLocked(int uid) { + if (mAudioOn) { + mAudioOn = false; + mAudioOnTimer.stopRunningLocked(this); + } + getUidStatsLocked(uid).noteAudioTurnedOffLocked(); + } + + public void noteVideoOnLocked(int uid) { + if (!mVideoOn) { + mVideoOn = true; + mVideoOnTimer.startRunningLocked(this); + } + getUidStatsLocked(uid).noteVideoTurnedOnLocked(); + } + + public void noteVideoOffLocked(int uid) { + if (mVideoOn) { + mVideoOn = false; + mVideoOnTimer.stopRunningLocked(this); + } + getUidStatsLocked(uid).noteVideoTurnedOffLocked(); + } + public void noteWifiRunningLocked() { if (!mWifiRunning) { mWifiRunning = true; @@ -1089,45 +1214,27 @@ public final class BatteryStatsImpl extends BatteryStats { } public void noteFullWifiLockAcquiredLocked(int uid) { - Uid u = mUidStats.get(uid); - if (u != null) { - u.noteFullWifiLockAcquiredLocked(); - } + getUidStatsLocked(uid).noteFullWifiLockAcquiredLocked(); } public void noteFullWifiLockReleasedLocked(int uid) { - Uid u = mUidStats.get(uid); - if (u != null) { - u.noteFullWifiLockReleasedLocked(); - } + getUidStatsLocked(uid).noteFullWifiLockReleasedLocked(); } public void noteScanWifiLockAcquiredLocked(int uid) { - Uid u = mUidStats.get(uid); - if (u != null) { - u.noteScanWifiLockAcquiredLocked(); - } + getUidStatsLocked(uid).noteScanWifiLockAcquiredLocked(); } public void noteScanWifiLockReleasedLocked(int uid) { - Uid u = mUidStats.get(uid); - if (u != null) { - u.noteScanWifiLockReleasedLocked(); - } + getUidStatsLocked(uid).noteScanWifiLockReleasedLocked(); } public void noteWifiMulticastEnabledLocked(int uid) { - Uid u = mUidStats.get(uid); - if (u != null) { - u.noteWifiMulticastEnabledLocked(); - } + getUidStatsLocked(uid).noteWifiMulticastEnabledLocked(); } public void noteWifiMulticastDisabledLocked(int uid) { - Uid u = mUidStats.get(uid); - if (u != null) { - u.noteWifiMulticastDisabledLocked(); - } + getUidStatsLocked(uid).noteWifiMulticastDisabledLocked(); } @Override public long getScreenOnTime(long batteryRealtime, int which) { @@ -1139,7 +1246,7 @@ public final class BatteryStatsImpl extends BatteryStats { return mScreenBrightnessTimer[brightnessBin].getTotalTimeLocked( batteryRealtime, which); } - + @Override public int getInputEventCount(int which) { return mInputEventCounter.getCountLocked(which); } @@ -1147,7 +1254,7 @@ public final class BatteryStatsImpl extends BatteryStats { @Override public long getPhoneOnTime(long batteryRealtime, int which) { return mPhoneOnTimer.getTotalTimeLocked(batteryRealtime, which); } - + @Override public long getPhoneSignalStrengthTime(int strengthBin, long batteryRealtime, int which) { return mPhoneSignalStrengthsTimer[strengthBin].getTotalTimeLocked( @@ -1214,9 +1321,15 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mScanWifiLockOut; StopwatchTimer mScanWifiLockTimer; - + boolean mWifiMulticastEnabled; StopwatchTimer mWifiMulticastTimer; + + boolean mAudioTurnedOn; + StopwatchTimer mAudioTurnedOnTimer; + + boolean mVideoTurnedOn; + StopwatchTimer mVideoTurnedOnTimer; Counter[] mUserActivityCounters; @@ -1247,6 +1360,8 @@ public final class BatteryStatsImpl extends BatteryStats { mScanWifiLockTimer = new StopwatchTimer(SCAN_WIFI_LOCK, null, mUnpluggables); mWifiMulticastTimer = new StopwatchTimer(WIFI_MULTICAST_ENABLED, null, mUnpluggables); + mAudioTurnedOnTimer = new StopwatchTimer(AUDIO_TURNED_ON, null, mUnpluggables); + mVideoTurnedOnTimer = new StopwatchTimer(VIDEO_TURNED_ON, null, mUnpluggables); } @Override @@ -1268,11 +1383,13 @@ public final class BatteryStatsImpl extends BatteryStats { public Map<String, ? extends BatteryStats.Uid.Pkg> getPackageStats() { return mPackageStats; } - + + @Override public int getUid() { return mUid; } - + + @Override public long getTcpBytesReceived(int which) { if (which == STATS_LAST) { return mLoadedTcpBytesReceived; @@ -1291,7 +1408,8 @@ public final class BatteryStatsImpl extends BatteryStats { return mCurrentTcpBytesReceived + (mStartedTcpBytesReceived >= 0 ? (NetStat.getUidRxBytes(mUid) - mStartedTcpBytesReceived) : 0); } - + + @Override public long getTcpBytesSent(int which) { if (which == STATS_LAST) { return mLoadedTcpBytesSent; @@ -1331,6 +1449,38 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override + public void noteVideoTurnedOnLocked() { + if (!mVideoTurnedOn) { + mVideoTurnedOn = true; + mVideoTurnedOnTimer.startRunningLocked(BatteryStatsImpl.this); + } + } + + @Override + public void noteVideoTurnedOffLocked() { + if (mVideoTurnedOn) { + mVideoTurnedOn = false; + mVideoTurnedOnTimer.stopRunningLocked(BatteryStatsImpl.this); + } + } + + @Override + public void noteAudioTurnedOnLocked() { + if (!mAudioTurnedOn) { + mAudioTurnedOn = true; + mAudioTurnedOnTimer.startRunningLocked(BatteryStatsImpl.this); + } + } + + @Override + public void noteAudioTurnedOffLocked() { + if (mAudioTurnedOn) { + mAudioTurnedOn = false; + mAudioTurnedOnTimer.stopRunningLocked(BatteryStatsImpl.this); + } + } + + @Override public void noteFullWifiLockReleasedLocked() { if (mFullWifiLockOut) { mFullWifiLockOut = false; @@ -1374,7 +1524,17 @@ public final class BatteryStatsImpl extends BatteryStats { public long getWifiTurnedOnTime(long batteryRealtime, int which) { return mWifiTurnedOnTimer.getTotalTimeLocked(batteryRealtime, which); } - + + @Override + public long getAudioTurnedOnTime(long batteryRealtime, int which) { + return mAudioTurnedOnTimer.getTotalTimeLocked(batteryRealtime, which); + } + + @Override + public long getVideoTurnedOnTime(long batteryRealtime, int which) { + return mVideoTurnedOnTimer.getTotalTimeLocked(batteryRealtime, which); + } + @Override public long getFullWifiLockTime(long batteryRealtime, int which) { return mFullWifiLockTimer.getTotalTimeLocked(batteryRealtime, which); @@ -1425,7 +1585,7 @@ public final class BatteryStatsImpl extends BatteryStats { return mCurrentTcpBytesSent + (mStartedTcpBytesSent >= 0 ? (NetStat.getUidTxBytes(mUid) - mStartedTcpBytesSent) : 0); } - + void writeToParcelLocked(Parcel out, long batteryRealtime) { out.writeInt(mWakelockStats.size()); for (Map.Entry<String, Uid.Wakelock> wakelockEntry : mWakelockStats.entrySet()) { @@ -1463,6 +1623,8 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeLong(mTcpBytesSentAtLastUnplug); mWifiTurnedOnTimer.writeToParcel(out, batteryRealtime); mFullWifiLockTimer.writeToParcel(out, batteryRealtime); + mAudioTurnedOnTimer.writeToParcel(out, batteryRealtime); + mVideoTurnedOnTimer.writeToParcel(out, batteryRealtime); mScanWifiLockTimer.writeToParcel(out, batteryRealtime); mWifiMulticastTimer.writeToParcel(out, batteryRealtime); if (mUserActivityCounters == null) { @@ -1522,6 +1684,10 @@ public final class BatteryStatsImpl extends BatteryStats { mWifiTurnedOnTimer = new StopwatchTimer(WIFI_TURNED_ON, null, mUnpluggables, in); mFullWifiLockOut = false; mFullWifiLockTimer = new StopwatchTimer(FULL_WIFI_LOCK, null, mUnpluggables, in); + mAudioTurnedOn = false; + mAudioTurnedOnTimer = new StopwatchTimer(AUDIO_TURNED_ON, null, mUnpluggables, in); + mVideoTurnedOn = false; + mVideoTurnedOnTimer = new StopwatchTimer(VIDEO_TURNED_ON, null, mUnpluggables, in); mScanWifiLockOut = false; mScanWifiLockTimer = new StopwatchTimer(SCAN_WIFI_LOCK, null, mUnpluggables, in); mWifiMulticastEnabled = false; @@ -1632,7 +1798,8 @@ public final class BatteryStatsImpl extends BatteryStats { public Timer getSensorTime() { return mTimer; } - + + @Override public int getHandle() { return mHandle; } @@ -1658,6 +1825,11 @@ public final class BatteryStatsImpl extends BatteryStats { int mStarts; /** + * Amount of time the process was running in the foreground. + */ + long mForegroundTime; + + /** * The amount of user time loaded from a previous save. */ long mLoadedUserTime; @@ -1673,6 +1845,11 @@ public final class BatteryStatsImpl extends BatteryStats { int mLoadedStarts; /** + * The amount of foreground time loaded from a previous save. + */ + long mLoadedForegroundTime; + + /** * The amount of user time loaded from the previous run. */ long mLastUserTime; @@ -1688,6 +1865,11 @@ public final class BatteryStatsImpl extends BatteryStats { int mLastStarts; /** + * The amount of foreground time loaded from the previous run + */ + long mLastForegroundTime; + + /** * The amount of user time when last unplugged. */ long mUnpluggedUserTime; @@ -1702,6 +1884,11 @@ public final class BatteryStatsImpl extends BatteryStats { */ int mUnpluggedStarts; + /** + * The amount of foreground time since unplugged. + */ + long mUnpluggedForegroundTime; + Proc() { mUnpluggables.add(this); } @@ -1710,6 +1897,7 @@ public final class BatteryStatsImpl extends BatteryStats { mUnpluggedUserTime = mUserTime; mUnpluggedSystemTime = mSystemTime; mUnpluggedStarts = mStarts; + mUnpluggedForegroundTime = mForegroundTime; } public void plug(long batteryUptime, long batteryRealtime) { @@ -1721,30 +1909,38 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeLong(mUserTime); out.writeLong(mSystemTime); + out.writeLong(mForegroundTime); out.writeInt(mStarts); out.writeLong(mLoadedUserTime); out.writeLong(mLoadedSystemTime); + out.writeLong(mLoadedForegroundTime); out.writeInt(mLoadedStarts); out.writeLong(mLastUserTime); out.writeLong(mLastSystemTime); + out.writeLong(mLastForegroundTime); out.writeInt(mLastStarts); out.writeLong(mUnpluggedUserTime); out.writeLong(mUnpluggedSystemTime); + out.writeLong(mUnpluggedForegroundTime); out.writeInt(mUnpluggedStarts); } void readFromParcelLocked(Parcel in) { mUserTime = in.readLong(); mSystemTime = in.readLong(); + mForegroundTime = in.readLong(); mStarts = in.readInt(); mLoadedUserTime = in.readLong(); mLoadedSystemTime = in.readLong(); + mLoadedForegroundTime = in.readLong(); mLoadedStarts = in.readInt(); mLastUserTime = in.readLong(); mLastSystemTime = in.readLong(); + mLastForegroundTime = in.readLong(); mLastStarts = in.readInt(); mUnpluggedUserTime = in.readLong(); mUnpluggedSystemTime = in.readLong(); + mUnpluggedForegroundTime = in.readLong(); mUnpluggedStarts = in.readInt(); } @@ -1757,6 +1953,10 @@ public final class BatteryStatsImpl extends BatteryStats { mSystemTime += stime; } + public void addForegroundTimeLocked(long ttime) { + mForegroundTime += ttime; + } + public void incStartsLocked() { mStarts++; } @@ -1794,6 +1994,22 @@ public final class BatteryStatsImpl extends BatteryStats { } @Override + public long getForegroundTime(int which) { + long val; + if (which == STATS_LAST) { + val = mLastForegroundTime; + } else { + val = mForegroundTime; + if (which == STATS_CURRENT) { + val -= mLoadedForegroundTime; + } else if (which == STATS_UNPLUGGED) { + val -= mUnpluggedForegroundTime; + } + } + return val; + } + + @Override public int getStarts(int which) { int val; if (which == STATS_LAST) { @@ -2315,7 +2531,7 @@ public final class BatteryStatsImpl extends BatteryStats { StopwatchTimer t = getSensorTimerLocked(Sensor.GPS, false); if (t != null) { t.stopRunningLocked(BatteryStatsImpl.this); - } + } } public BatteryStatsImpl getBatteryStats() { @@ -2526,7 +2742,44 @@ public final class BatteryStatsImpl extends BatteryStats { public long getBatteryRealtime(long curTime) { return getBatteryRealtimeLocked(curTime); } - + + private long getTcpBytes(long current, long[] dataBytes, int which) { + if (which == STATS_LAST) { + return dataBytes[STATS_LAST]; + } else { + if (which == STATS_UNPLUGGED) { + if (dataBytes[STATS_UNPLUGGED] < 0) { + return dataBytes[STATS_LAST]; + } else { + return current - dataBytes[STATS_UNPLUGGED]; + } + } else if (which == STATS_TOTAL) { + return (current - dataBytes[STATS_CURRENT]) + dataBytes[STATS_TOTAL]; + } + return current - dataBytes[STATS_CURRENT]; + } + } + + /** Only STATS_UNPLUGGED works properly */ + public long getMobileTcpBytesSent(int which) { + return getTcpBytes(NetStat.getMobileTxBytes(), mMobileDataTx, which); + } + + /** Only STATS_UNPLUGGED works properly */ + public long getMobileTcpBytesReceived(int which) { + return getTcpBytes(NetStat.getMobileRxBytes(), mMobileDataRx, which); + } + + /** Only STATS_UNPLUGGED works properly */ + public long getTotalTcpBytesSent(int which) { + return getTcpBytes(NetStat.getTotalTxBytes(), mTotalDataTx, which); + } + + /** Only STATS_UNPLUGGED works properly */ + public long getTotalTcpBytesReceived(int which) { + return getTcpBytes(NetStat.getTotalRxBytes(), mTotalDataRx, which); + } + @Override public int getDischargeStartLevel() { synchronized(this) { @@ -2567,7 +2820,7 @@ public final class BatteryStatsImpl extends BatteryStats { public void removeUidStatsLocked(int uid) { mUidStats.remove(uid); } - + /** * Retrieve the statistics object for a particular process, creating * if needed. @@ -2578,6 +2831,24 @@ public final class BatteryStatsImpl extends BatteryStats { } /** + * Retrieve the statistics object for a particular process, given + * the name of the process. + * @param name process name + * @return the statistics object for the process + */ + public Uid.Proc getProcessStatsLocked(String name, int pid) { + int uid; + if (mUidCache.containsKey(name)) { + uid = mUidCache.get(name); + } else { + uid = Process.getUidForPid(pid); + mUidCache.put(name, uid); + } + Uid u = getUidStatsLocked(uid); + return u.getProcessStatsLocked(name); + } + + /** * Retrieve the statistics object for a particular process, creating * if needed. */ @@ -2752,6 +3023,10 @@ public final class BatteryStatsImpl extends BatteryStats { u.mWifiTurnedOnTimer.readSummaryFromParcelLocked(in); u.mFullWifiLockOut = false; u.mFullWifiLockTimer.readSummaryFromParcelLocked(in); + u.mAudioTurnedOn = false; + u.mAudioTurnedOnTimer.readSummaryFromParcelLocked(in); + u.mVideoTurnedOn = false; + u.mVideoTurnedOnTimer.readSummaryFromParcelLocked(in); u.mScanWifiLockOut = false; u.mScanWifiLockTimer.readSummaryFromParcelLocked(in); u.mWifiMulticastEnabled = false; @@ -2888,6 +3163,8 @@ public final class BatteryStatsImpl extends BatteryStats { u.mWifiTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); u.mFullWifiLockTimer.writeSummaryFromParcelLocked(out, NOWREAL); + u.mAudioTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); + u.mVideoTurnedOnTimer.writeSummaryFromParcelLocked(out, NOWREAL); u.mScanWifiLockTimer.writeSummaryFromParcelLocked(out, NOWREAL); u.mWifiMulticastTimer.writeSummaryFromParcelLocked(out, NOWREAL); @@ -3046,11 +3323,24 @@ public final class BatteryStatsImpl extends BatteryStats { mDischargeCurrentLevel = in.readInt(); mLastWriteTime = in.readLong(); + mMobileDataRx[STATS_LAST] = in.readLong(); + mMobileDataRx[STATS_UNPLUGGED] = -1; + mMobileDataTx[STATS_LAST] = in.readLong(); + mMobileDataTx[STATS_UNPLUGGED] = -1; + mTotalDataRx[STATS_LAST] = in.readLong(); + mTotalDataRx[STATS_UNPLUGGED] = -1; + mTotalDataTx[STATS_LAST] = in.readLong(); + mTotalDataTx[STATS_UNPLUGGED] = -1; + + mRadioDataUptime = in.readLong(); + mRadioDataStart = -1; + mKernelWakelockStats.clear(); int NKW = in.readInt(); for (int ikw = 0; ikw < NKW; ikw++) { if (in.readInt() != 0) { String wakelockName = in.readString(); + in.readInt(); // Extra 0/1 written by Timer.writeTimerToParcel SamplingTimer kwlt = new SamplingTimer(mUnpluggables, mOnBattery, in); mKernelWakelockStats.put(wakelockName, kwlt); } @@ -3119,6 +3409,14 @@ public final class BatteryStatsImpl extends BatteryStats { out.writeInt(mDischargeCurrentLevel); out.writeLong(mLastWriteTime); + out.writeLong(getMobileTcpBytesReceived(STATS_UNPLUGGED)); + out.writeLong(getMobileTcpBytesSent(STATS_UNPLUGGED)); + out.writeLong(getTotalTcpBytesReceived(STATS_UNPLUGGED)); + out.writeLong(getTotalTcpBytesSent(STATS_UNPLUGGED)); + + // Write radio uptime for data + out.writeLong(getRadioDataUptimeMs()); + out.writeInt(mKernelWakelockStats.size()); for (Map.Entry<String, SamplingTimer> ent : mKernelWakelockStats.entrySet()) { SamplingTimer kwlt = ent.getValue(); diff --git a/core/java/com/android/internal/os/PowerProfile.java b/core/java/com/android/internal/os/PowerProfile.java new file mode 100644 index 0000000..4a8d8b1 --- /dev/null +++ b/core/java/com/android/internal/os/PowerProfile.java @@ -0,0 +1,238 @@ +/* + * 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 com.android.internal.os; + + +import android.content.Context; +import android.content.res.XmlResourceParser; + +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Reports power consumption values for various device activities. Reads values from an XML file. + * Customize the XML file for different devices. + * [hidden] + */ +public class PowerProfile { + + /** + * No power consumption, or accounted for elsewhere. + */ + public static final String POWER_NONE = "none"; + + /** + * Power consumption when CPU is in power collapse mode. + */ + public static final String POWER_CPU_IDLE = "cpu.idle"; + + /** + * Power consumption when CPU is running at normal speed. + */ + public static final String POWER_CPU_NORMAL = "cpu.normal"; + + /** + * Power consumption when CPU is running at full speed. + */ + public static final String POWER_CPU_FULL = "cpu.full"; + + /** + * Power consumption when WiFi driver is scanning for networks. + */ + public static final String POWER_WIFI_SCAN = "wifi.scan"; + + /** + * Power consumption when WiFi driver is on. + */ + public static final String POWER_WIFI_ON = "wifi.on"; + + /** + * Power consumption when WiFi driver is transmitting/receiving. + */ + public static final String POWER_WIFI_ACTIVE = "wifi.active"; + + /** + * Power consumption when GPS is on. + */ + public static final String POWER_GPS_ON = "gps.on"; + + /** + * Power consumption when Bluetooth driver is on. + */ + public static final String POWER_BLUETOOTH_ON = "bluetooth.on"; + + /** + * Power consumption when Bluetooth driver is transmitting/receiving. + */ + public static final String POWER_BLUETOOTH_ACTIVE = "bluetooth.active"; + + /** + * Power consumption when screen is on, not including the backlight power. + */ + public static final String POWER_SCREEN_ON = "screen.on"; + + /** + * Power consumption when cell radio is on but not on a call. + */ + public static final String POWER_RADIO_ON = "radio.on"; + + /** + * Power consumption when talking on the phone. + */ + public static final String POWER_RADIO_ACTIVE = "radio.active"; + + /** + * Power consumption at full backlight brightness. If the backlight is at + * 50% brightness, then this should be multiplied by 0.5 + */ + public static final String POWER_SCREEN_FULL = "screen.full"; + + /** + * Power consumed by the audio hardware when playing back audio content. This is in addition + * to the CPU power, probably due to a DSP and / or amplifier. + */ + public static final String POWER_AUDIO = "dsp.audio"; + + /** + * Power consumed by any media hardware when playing back video content. This is in addition + * to the CPU power, probably due to a DSP. + */ + public static final String POWER_VIDEO = "dsp.video"; + + static final HashMap<String, Object> sPowerMap = new HashMap<String, Object>(); + + private static final String TAG_DEVICE = "device"; + private static final String TAG_ITEM = "item"; + private static final String TAG_ARRAY = "array"; + private static final String TAG_ARRAYITEM = "value"; + private static final String ATTR_NAME = "name"; + + public PowerProfile(Context context) { + // Read the XML file for the given profile (normally only one per + // device) + if (sPowerMap.size() == 0) { + readPowerValuesFromXml(context); + } + } + + private void readPowerValuesFromXml(Context context) { + int id = com.android.internal.R.xml.power_profile; + XmlResourceParser parser = context.getResources().getXml(id); + boolean parsingArray = false; + ArrayList<Double> array = new ArrayList<Double>(); + String arrayName = null; + + try { + XmlUtils.beginDocument(parser, TAG_DEVICE); + + while (true) { + XmlUtils.nextElement(parser); + + String element = parser.getName(); + if (element == null) break; + + if (parsingArray && !element.equals(TAG_ARRAYITEM)) { + // Finish array + sPowerMap.put(arrayName, array.toArray(new Double[array.size()])); + parsingArray = false; + } + if (element.equals(TAG_ARRAY)) { + parsingArray = true; + array.clear(); + arrayName = parser.getAttributeValue(null, ATTR_NAME); + } else if (element.equals(TAG_ITEM) || element.equals(TAG_ARRAYITEM)) { + String name = null; + if (!parsingArray) name = parser.getAttributeValue(null, ATTR_NAME); + if (parser.next() == XmlPullParser.TEXT) { + String power = parser.getText(); + double value = 0; + try { + value = Double.valueOf(power); + } catch (NumberFormatException nfe) { + } + if (element.equals(TAG_ITEM)) { + sPowerMap.put(name, value); + } else if (parsingArray) { + array.add(value); + } + } + } + } + if (parsingArray) { + sPowerMap.put(arrayName, array.toArray(new Double[array.size()])); + } + } catch (XmlPullParserException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + parser.close(); + } + } + + /** + * Returns the average current in mA consumed by the subsystem + * @param type the subsystem type + * @return the average current in milliAmps. + */ + public double getAveragePower(String type) { + if (sPowerMap.containsKey(type)) { + Object data = sPowerMap.get(type); + if (data instanceof Double[]) { + return ((Double[])data)[0]; + } else { + return (Double) sPowerMap.get(type); + } + } else { + return 0; + } + } + + /** + * Returns the average current in mA consumed by the subsystem for the given level. + * @param type the subsystem type + * @param level the level of power at which the subsystem is running. For instance, the + * signal strength of the cell network between 0 and 4 (if there are 4 bars max.). + * If there is no data for multiple levels, the level is ignored. + * @return the average current in milliAmps. + */ + public double getAveragePower(String type, int level) { + if (sPowerMap.containsKey(type)) { + Object data = sPowerMap.get(type); + if (data instanceof Double[]) { + final Double[] values = (Double[]) data; + if (values.length > level && level >= 0) { + return values[level]; + } else if (level < 0) { + return 0; + } else { + return values[values.length - 1]; + } + } else { + return (Double) data; + } + } else { + return 0; + } + } +} diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index ac8b589..f67a235 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -467,7 +467,7 @@ public class ZygoteInit { "--setuid=1000", "--setgid=1000", "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,3001,3002,3003", - "--capabilities=121715744,121715744", + "--capabilities=130104352,130104352", "--runtime-init", "--nice-name=system_server", "com.android.server.SystemServer", diff --git a/core/java/com/android/internal/util/BitwiseInputStream.java b/core/java/com/android/internal/util/BitwiseInputStream.java index 4757919..86f74f3 100644 --- a/core/java/com/android/internal/util/BitwiseInputStream.java +++ b/core/java/com/android/internal/util/BitwiseInputStream.java @@ -65,30 +65,31 @@ public class BitwiseInputStream { /** * Read some data and increment the current position. * - * @param bits the amount of data to read (gte 0, lte 8) + * The 8-bit limit on access to bitwise streams is intentional to + * avoid endianness issues. * + * @param bits the amount of data to read (gte 0, lte 8) * @return byte of read data (possibly partially filled, from lsb) */ - public byte read(int bits) throws AccessException { + public int read(int bits) throws AccessException { int index = mPos >>> 3; int offset = 16 - (mPos & 0x07) - bits; // &7==%8 if ((bits < 0) || (bits > 8) || ((mPos + bits) > mEnd)) { throw new AccessException("illegal read " + "(pos " + mPos + ", end " + mEnd + ", bits " + bits + ")"); } - int data = (mBuf[index] & 0x00FF) << 8; - if (offset < 8) data |= (mBuf[index + 1] & 0xFF); + int data = (mBuf[index] & 0xFF) << 8; + if (offset < 8) data |= mBuf[index + 1] & 0xFF; data >>>= offset; data &= (-1 >>> (32 - bits)); mPos += bits; - return (byte)data; + return data; } /** * Read data in bulk into a byte array and increment the current position. * * @param bits the amount of data to read - * * @return newly allocated byte array of read data */ public byte[] readByteArray(int bits) throws AccessException { diff --git a/core/java/com/android/internal/util/BitwiseOutputStream.java b/core/java/com/android/internal/util/BitwiseOutputStream.java index 1b974ce..70c0be8 100644 --- a/core/java/com/android/internal/util/BitwiseOutputStream.java +++ b/core/java/com/android/internal/util/BitwiseOutputStream.java @@ -82,6 +82,9 @@ public class BitwiseOutputStream { /** * Write some data and increment the current position. * + * The 8-bit limit on access to bitwise streams is intentional to + * avoid endianness issues. + * * @param bits the amount of data to write (gte 0, lte 8) * @param data to write, will be masked to expose only bits param from lsb */ @@ -95,8 +98,8 @@ public class BitwiseOutputStream { int offset = 16 - (mPos & 0x07) - bits; // &7==%8 data <<= offset; mPos += bits; - mBuf[index] |= (data >>> 8); - if (offset < 8) mBuf[index + 1] |= (data & 0x00FF); + mBuf[index] |= data >>> 8; + if (offset < 8) mBuf[index + 1] |= data & 0xFF; } /** diff --git a/core/java/com/google/android/net/GoogleHttpClient.java b/core/java/com/google/android/net/GoogleHttpClient.java index 871c925..922f5be 100644 --- a/core/java/com/google/android/net/GoogleHttpClient.java +++ b/core/java/com/google/android/net/GoogleHttpClient.java @@ -37,6 +37,10 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.LayeredSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.scheme.SocketFactory; import org.apache.http.impl.client.EntityEnclosingRequestWrapper; import org.apache.http.impl.client.RequestWrapper; import org.apache.http.params.HttpParams; @@ -44,6 +48,8 @@ import org.apache.http.protocol.HttpContext; import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; @@ -66,25 +72,22 @@ public class GoogleHttpClient implements HttpClient { private final AndroidHttpClient mClient; private final ContentResolver mResolver; - private final String mUserAgent; + private final String mAppName, mUserAgent; + private final ThreadLocal<Boolean> mConnectionAllocated = new ThreadLocal<Boolean>(); /** - * Create an HTTP client. Normally one client is shared throughout an app. - * @param resolver to use for accessing URL rewriting rules. - * @param userAgent to report in your HTTP requests. - * @deprecated Use {@link #GoogleHttpClient(android.content.ContentResolver, String, boolean)} + * Create an HTTP client without SSL session persistence. + * @deprecated Use {@link #GoogleHttpClient(android.content.Context, String, boolean)} */ public GoogleHttpClient(ContentResolver resolver, String userAgent) { mClient = AndroidHttpClient.newInstance(userAgent); mResolver = resolver; - mUserAgent = userAgent; + mUserAgent = mAppName = userAgent; } /** - * GoogleHttpClient(Context, String, boolean) - without SSL session - * persistence. - * - * @deprecated use Context instead of ContentResolver. + * Create an HTTP client without SSL session persistence. + * @deprecated Use {@link #GoogleHttpClient(android.content.Context, String, boolean)} */ public GoogleHttpClient(ContentResolver resolver, String appAndVersion, boolean gzipCapable) { @@ -111,21 +114,72 @@ public class GoogleHttpClient implements HttpClient { * headers. Needed because Google servers require gzip in the User-Agent * in order to return gzip'd content. */ - public GoogleHttpClient(Context context, String appAndVersion, - boolean gzipCapable) { - this(context.getContentResolver(), SSLClientSessionCacheFactory.getCache(context), + public GoogleHttpClient(Context context, String appAndVersion, boolean gzipCapable) { + this(context.getContentResolver(), + SSLClientSessionCacheFactory.getCache(context), appAndVersion, gzipCapable); } - private GoogleHttpClient(ContentResolver resolver, SSLClientSessionCache cache, + private GoogleHttpClient(ContentResolver resolver, + SSLClientSessionCache cache, String appAndVersion, boolean gzipCapable) { String userAgent = appAndVersion + " (" + Build.DEVICE + " " + Build.ID + ")"; if (gzipCapable) { userAgent = userAgent + "; gzip"; } + mClient = AndroidHttpClient.newInstance(userAgent, cache); mResolver = resolver; + mAppName = appAndVersion; mUserAgent = userAgent; + + // Wrap all the socket factories with the appropriate wrapper. (Apache + // HTTP, curse its black and stupid heart, inspects the SocketFactory to + // see if it's a LayeredSocketFactory, so we need two wrapper classes.) + SchemeRegistry registry = getConnectionManager().getSchemeRegistry(); + for (String name : registry.getSchemeNames()) { + Scheme scheme = registry.unregister(name); + SocketFactory sf = scheme.getSocketFactory(); + if (sf instanceof LayeredSocketFactory) { + sf = new WrappedLayeredSocketFactory((LayeredSocketFactory) sf); + } else { + sf = new WrappedSocketFactory(sf); + } + registry.register(new Scheme(name, sf, scheme.getDefaultPort())); + } + } + + /** + * Delegating wrapper for SocketFactory records when sockets are connected. + * We use this to know whether a connection was created vs reused, to + * gather per-app statistics about connection reuse rates. + * (Note, we record only *connection*, not *creation* of sockets -- + * what we care about is the network overhead of an actual TCP connect.) + */ + private class WrappedSocketFactory implements SocketFactory { + private SocketFactory mDelegate; + private WrappedSocketFactory(SocketFactory delegate) { mDelegate = delegate; } + public final Socket createSocket() throws IOException { return mDelegate.createSocket(); } + public final boolean isSecure(Socket s) { return mDelegate.isSecure(s); } + + public final Socket connectSocket( + Socket s, String h, int p, + InetAddress la, int lp, HttpParams params) throws IOException { + mConnectionAllocated.set(Boolean.TRUE); + return mDelegate.connectSocket(s, h, p, la, lp, params); + } + } + + /** Like WrappedSocketFactory, but for the LayeredSocketFactory subclass. */ + private class WrappedLayeredSocketFactory + extends WrappedSocketFactory implements LayeredSocketFactory { + private LayeredSocketFactory mDelegate; + private WrappedLayeredSocketFactory(LayeredSocketFactory sf) { super(sf); mDelegate = sf; } + + public final Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + return mDelegate.createSocket(s, host, port, autoClose); + } } /** @@ -140,24 +194,21 @@ public class GoogleHttpClient implements HttpClient { public HttpResponse executeWithoutRewriting( HttpUriRequest request, HttpContext context) throws IOException { - String code = "Error"; + int code = -1; long start = SystemClock.elapsedRealtime(); try { HttpResponse response; - // TODO: if we're logging network stats, and if the apache library is configured - // to follow redirects, count each redirect as an additional round trip. + mConnectionAllocated.set(null); - // see if we're logging network stats. - boolean logNetworkStats = NetworkStatsEntity.shouldLogNetworkStats(); + if (NetworkStatsEntity.shouldLogNetworkStats()) { + // TODO: if we're logging network stats, and if the apache library is configured + // to follow redirects, count each redirect as an additional round trip. - if (logNetworkStats) { int uid = android.os.Process.myUid(); long startTx = NetStat.getUidTxBytes(uid); long startRx = NetStat.getUidRxBytes(uid); response = mClient.execute(request, context); - code = Integer.toString(response.getStatusLine().getStatusCode()); - HttpEntity origEntity = response == null ? null : response.getEntity(); if (origEntity != null) { // yeah, we compute the same thing below. we do need to compute this here @@ -165,30 +216,39 @@ public class GoogleHttpClient implements HttpClient { long now = SystemClock.elapsedRealtime(); long elapsed = now - start; NetworkStatsEntity entity = new NetworkStatsEntity(origEntity, - mUserAgent, uid, startTx, startRx, + mAppName, uid, startTx, startRx, elapsed /* response latency */, now /* processing start time */); response.setEntity(entity); } } else { response = mClient.execute(request, context); - code = Integer.toString(response.getStatusLine().getStatusCode()); } + code = response.getStatusLine().getStatusCode(); return response; - } catch (IOException e) { - code = "IOException"; - throw e; } finally { // Record some statistics to the checkin service about the outcome. // Note that this is only describing execute(), not body download. + // We assume the database writes are much faster than network I/O, + // and not worth running in a background thread or anything. try { long elapsed = SystemClock.elapsedRealtime() - start; ContentValues values = new ContentValues(); - values.put(Checkin.Stats.TAG, - Checkin.Stats.Tag.HTTP_STATUS + ":" + - mUserAgent + ":" + code); values.put(Checkin.Stats.COUNT, 1); values.put(Checkin.Stats.SUM, elapsed / 1000.0); + + values.put(Checkin.Stats.TAG, Checkin.Stats.Tag.HTTP_REQUEST + ":" + mAppName); + mResolver.insert(Checkin.Stats.CONTENT_URI, values); + + // No sockets and no exceptions means we successfully reused a connection + if (mConnectionAllocated.get() == null && code >= 0) { + values.put(Checkin.Stats.TAG, Checkin.Stats.Tag.HTTP_REUSED + ":" + mAppName); + mResolver.insert(Checkin.Stats.CONTENT_URI, values); + } + + String status = code < 0 ? "IOException" : Integer.toString(code); + values.put(Checkin.Stats.TAG, + Checkin.Stats.Tag.HTTP_STATUS + ":" + mAppName + ":" + status); mResolver.insert(Checkin.Stats.CONTENT_URI, values); } catch (Exception e) { Log.e(TAG, "Error recording stats", e); diff --git a/core/java/com/google/android/util/GoogleWebContentHelper.java b/core/java/com/google/android/util/GoogleWebContentHelper.java index 2911420..8291e29 100644 --- a/core/java/com/google/android/util/GoogleWebContentHelper.java +++ b/core/java/com/google/android/util/GoogleWebContentHelper.java @@ -130,7 +130,18 @@ public class GoogleWebContentHelper { mWebView.loadUrl(mSecureUrl); return this; } - + + /** + * Loads data into the webview and also provides a failback url + * @return This {@link GoogleWebContentHelper} so methods can be chained. + */ + public GoogleWebContentHelper loadDataWithFailUrl(String base, String data, + String mimeType, String encoding, String failUrl) { + ensureViews(); + mWebView.loadDataWithBaseURL(base, data, mimeType, encoding, failUrl); + return this; + } + /** * Helper to handle the back key. Returns true if the back key was handled, * otherwise returns false. |
